20210112のSwiftに関する記事は3件です。

iOS「自動更新サブスクリプション」のサーバー通知をRubyで受け取る

iOS「自動更新サブスクリプション」のサーバー通知をRubyで受け取る

iOS「自動更新サブスクリプション」のサーバー通知をRubyで受け取る必要があり、いろいろ調べたので備忘録としてまとめてみます!

構成

マルチプラットフォーム型のサービス
iOSアプリ:Swift
サーバーサイド:Ruby (Ruby on Rails)
※サーバーサイドでの決済システムは他のサービスを利用

おさらい:サーバー通知とは

サーバーサイドのURLをAppStoreConnectに登録しておくことで、自動更新サブスクリプションのステータスが変更したときにAppleから通知を取得することができます。

この機能があることで、入会・解約・アップグレード・ダウングレードなども配信者側はオンタイムで確認・知ることができます。

サーバー通知を受信するURL(サーバー)の登録

AppStoreConnect > 該当アプリページ を開きます。
該当アプリページのApp情報(名前などを登録するところ)の右下にある「App Storeサーバー通知のURL」にURLを登録すると、後述のタイミングでAppleStoreからの通知が登録したURLに送られてくるようになります!

サーバー通知が送られてくるタイミング

ユーザーのイベント 通知タイプ
初回購入 INITIAL_BUY
アップグレード CANCEL,DID_CHANGE_RENEWAL_STATUS, INTERACTIVE_RENEWAL
ダウングレード DID_CHANGE_RENEWAL_PREF
期限が切れた後に再購読 DID_CHANGE_RENEWAL_STATUS
期限が切れた後に別のサブスクを購読 INTERACTIVE_RENEWAL, DID_CHANGE_RENEWAL_STATUS
購読をキャンセル DID_CHANGE_RENEWAL_STATUS
Appleによる返金 CANCEL,DID_CHANGE_RENEWAL_STATUS
決済の問題でサブスクリプションの更新に失敗 DID_FAIL_TO_RENEW
ユーザーへ払い戻し REFUND
サブスクリプションの値上げに同意した PRICE_INCREASE_CONSENT
自動更新が成功 DID_RENEW

送られてくるJSONの形式

{
    "auto_renew_product_id": " ",
    "auto_renew_status": "false",
    "auto_renew_status_change_date": "2000-00-00 00:00:00 Etc/GMT",
    "auto_renew_status_change_date_ms": "0000000000000",
    "auto_renew_status_change_date_pst": "2000-00-00 00:00:00America/Los_Angeles",
    "environment": "Sandbox",
    "latest_receipt": " ", // Base64エンコードされたレシート 
    "latest_receipt_info": {
        "bid": " ",
        "bvrs": "1",
        "cancellation_date": "0000000000000",
        "expires_date": "0000000000000",
        "expires_date_formatted": "2000-00-00 00:00:00 Etc/GMT",
        "expires_date_formatted_pst": "2000-00-00 00:00:00 America/Los_Angeles",
        "is_in_intro_offer_period": "false",
        "is_trial_period": "false",
        "item_id": "1486750613",
        "original_purchase_date": "2000-00-00 00:00:00 Etc/GMT",
        "original_purchase_date_ms": "0000000000000",
        "original_purchase_date_pst": "2000-00-00 00:00:00 America/Los_Angeles",
        "original_transaction_id": "000000000000000",
        "product_id": " ",
        "purchase_date": "2000-00-00 00:00:00 Etc/GMT",
        "purchase_date_ms": "0000000000000",
        "purchase_date_pst": "2000-00-00 00:00:00America/Los_Angeles",
        "quantity": "1",
        "subscription_group_identifier": "00000000",
        "transaction_id": "000000000000000",
        "unique_identifier": "3bf0388bbca73e22cbfbce8e7b5b4c8181047dbc",
        "unique_vendor_identifier": "D89BF8D6-3B46-4F1E-895A-1061ECD31178",
        "version_external_identifier": "0",
        "web_order_line_item_id": "000000000000000"
    },
    "notification_type": "INITIAL_BUY",
    "password": " "
}

送られてくるJSONの各プロパティの意味

基本はこちらの公式サイトを見ることでわかります!
公式参考サイト

主要で利用したい値だけ今回まとめてみます
※オフィシャルサイトの英文をグーグル翻訳しただけの情報になります。正確な情報は公式サイトを確認してください。

フィールド名 意味
auto_renew_status 自動更新可能なサブスクリプション製品の現在の更新ステータス
auto_renew_status_change_date ユーザーが自動更新可能なサブスクリプションの更新ステータスをオンまたはオフにした時刻。
original_transaction_id 最初の購入のトランザクションID。この値は、ユーザーが購入を復元するか、サブスクリプションを更新する場合を除いて、同じです。
environment AppStoreがレシートを生成した環境。
latest_receipt Base64でエンコードされた最新のトランザクションレシート
latest_receipt_info 値のJSON表現。このフィールドはレシートの配列ですが、サーバー間通知では単一のオブジェクトです
cancellation_date アップルカスタマーサポートがトランザクションをキャンセルした時刻。このフィールドは、返金されたトランザクションにのみ表示されます。
cancellation_reason 返金された取引の理由。顧客がトランザクションをキャンセルすると、App Storeは顧客に返金を行い、このキーの値を提供します。の値は“1” 、アプリ内の実際の問題または認識された問題が原因で顧客がトランザクションをキャンセルしたことを示します。の値は“0” 、トランザクションが別の理由でキャンセルされたことを示します。たとえば、顧客が誤って購入した場合です。
expires_date サブスクリプションの有効期限が切れる時間、またはサブスクリプションが更新される時間
is_trial_period サブスクリプションが無料試用期間内にあるかどうかの指標。
is_upgraded ユーザーがアップグレードしたためにシステムがサブスクリプションをキャンセルしたことを示すインジケーター。このフィールドは、アップグレードトランザクションの場合にのみ表示されます。
product_id 購入した製品の一意の識別子。この値は、App Store Connectで製品を作成するときに指定し、トランザクションのプロパティに格納されているオブジェクトのプロパティに対応します。
purchase_date App Storeが、ISO 8601標準と同様の日時形式で、サブスクリプションの購入または更新に対してユーザーのアカウントに請求した時刻。
quantity 購入した消耗品の数。
subscription_group_identifier サブスクリプションが属するサブスクリプショングループの識別子。このフィールドの値は、SKProductのプロパティと同じです。
transaction_id 購入、復元、更新などのトランザクションの一意の識別子
notification_type 通知をトリガーしたサブスクリプションイベント
password レシートを検証するときにrequestBodyのpasswordフィールドに送信する共有シークレットと同じ値

送られてくるJSONをRubyで処理するコード

一言で言うと、上記のJSON形式を処理し、タイミング・各プロパティの値に合わせてアプリの処理を記載します。

#例としてreceive_appserver_notificationメソッドとして作成します
def receive_appserver_notification
    unified_receipt = params['unified_receipt'].as_json
    latest_receipt = unified_receipt['latest_receipt']
    latest_receipt_info = unified_receipt['latest_receipt_info']

   @mTestObj = TestObj.new()  #保存用のモデル、オブジェクトを用意
   @mTestObj.auto_renew_product_id = unified_receipt['auto_renew_product_id']
   @mTestObj.expires_date = latest_receipt_info['expires_date']
   #==以下省略==

   #処理があれば記載
   if @mTestObj.notification_type == 'INITIAL_BUY'
       #例えば、初期購入だった場合の処理・・・  
   end

   #保存
   begin 
      @mTestObj.save
      puts "保存完了!!"
   rescue => exception

   end

end


まとめ

1.AppStoreConnectで受信するURLを設定しましょう
2.送られてくる形式、パラメーターを理解しましょう
3.サーバーで受け取り、jsonを処理しましょう
4.値に合わせて、必要な処理を実装しましょう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリで位置情報を取得するときに配慮する点をまとめてみた③

前回の続き↓
iOSアプリで位置情報を取得するときに配慮する点をまとめてみた②

はじめに

iOSアプリで位置情報を取得するときに配慮する点をまとめた内容となっています。

前回までのあらすじ

前回、アプリの位置情報サービスの有無バックグラウンド状態から戻った時も
チェックをしなければいけないと書きました。

アプリがバックグラウンド状態から戻った時にある処理をするにはAppDelegateapplicationWillEnterForeground(_ application: UIApplication)で処理するんでしたね。

そして、アラートを表示したいのでViewController内で処理するためにNotificationCenterを使いました。

バックグラウンド状態から戻ったときにチェックする(アプリ)

では、実際にコードを書いてみます。
※長くなるので前回のソースコードから一部抜粋

今回も、アラートの表示はこの方法を用います↓
UIAlertControllerをファイルを分けて実装してみる

ViewController
import UIKit
import CoreLocation

final class ViewController: UIViewController {

   private var locationManager: CLLocationManager = {
        var locationManager = CLLocationManager()
        locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
        locationManager.distanceFilter = 5
        return locationManager
    }()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    // バックグラウンド状態から戻ってきた時に呼ばれるメソッド(自分で作る)
    @objc private func willEnterForeground() {
        // 端末の位置情報サービスがオンの場合      
        if CLLocationManager.locationServicesEnabled() {
            // 位置情報サービスの認証ステータスを取得
            let status = CLLocationManager.authorizationStatus()

            switch status {

            // 許可しない場合
            case .denied:
                // アラート表示
                Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                    // 設定アプリに画面遷移する
                    UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                }

            default:
                break
            }
        }

    // 端末の位置情報サービスがオフの場合
    }else {
        // アラート表示
        Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") 
    }
}

端末の位置情報サービスがオンかオフかを分岐して
オンの場合にアプリの位置情報サービスの有無をチェックしています。

ビルドしてみると、ちゃんとチェックされてますね↓

※バグ?のせいなのか、たまに位置情報サービスの認証ステータスが次回確認になっていますが
本来はなしになります。
ezgif.com-gif-maker3.gif
因みに、この部分は.deniedだけのケースのみでOKです。

理由は認証ステータスが変更されればデリゲートメソッドが呼ばれますし、設定アプリに画面遷移するのは.deniedの場合のみで、その時に何もしないで戻った場合をチェックしたいので他の認証ステータスの記述は必要ないです。

ここで、一旦、全体のソースコードを見てみましょう。

ViewController
import UIKit
import CoreLocation

final class ViewController: UIViewController {

    private var locationManager: CLLocationManager = {
       var locationManager = CLLocationManager()
       locationManager.distanceFilter = 5
       locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
       return locationManager
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        locationManager.requestWhenInUseAuthorization()
        locationManager.delegate = self
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    @objc private func willEnterForeground() {     
        if CLLocationManager.locationServicesEnabled() {

            let status = CLLocationManager.authorizationStatus()

            switch status {

            case .denied:
                Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                    UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                }

            default:
                break
            }

    }else {
        Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") 
    }
  }
}

extension ViewController: CLLocationManagerDelegate {
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
          if CLLocationManager.locationServicesEnabled() {

        let status = manager.authorizationStatus

        switch status {
        case .authorizedAlways, .authorizedWhenInUse:
            manager.startUpdatingLocation()

        case .notDetermined:
            manager.requestWhenInUseAuthorization()

        case .denied:
            Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
            }

        case .restricted:
            break           

        default:
            break
        }
      }else {
         Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") 
      }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let gps = manager.location?.coordinate else {
            return
        }
        manager.stopUpdatingLocation()
        let lat = gps.latitude
        let lng = gps.longitude
        print("経度:\(String(describing: lat)), 緯度:\(String(describing: lng))")
    }
}
Alert
import UIKit

final class Alert {
    static func okAlert(vc: UIViewController,title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) {
        let okAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
        okAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: handler))
        vc.present(okAlertVC, animated: true, completion: nil)
    }
}

端末・アプリの位置情報サービスの有無や、変更された認証ステータスの処理等ここまで
色んな所に配慮してきました。

後は、お気づきの方がいらっしゃると思いますが
認証ステータスが.restrictedの場合は何もしていませんね。

ここは、Appleのドキュメントを見てみるとペアレンタルコントロールなどの何らかの制限があるために
ユーザーが認証ステータスを変更できない状態に呼ばれる認証ステータスとなっています↓
CLAuthorization Status .restricted

ずっとbreakにしていましたが、アラートなどでユーザーに伝えたほうがいいので改修しときましょう。

ViewController
      case .restricted:
          Alert.okAlert(vc: self, title: "位置情報サービスの使用を\n許可されていません", message: "何らかの制限が掛かっています")

iOS14以外のバージョン対応

現状、iOS14以降を想定して位置情報の取得をしてきましたが
iOS14以外のバージョンでも位置情報を取得したい場合が開発していると出てくると思います。

その場合、使用するメソッドが違ったり、Appleが非推奨にしているコード等も登場して
多少、複雑になってきます。

では、実際にアプリのバージョンを下げてみて実装していきましょう。

私はこちらの記事を拝見して、iOSのバージョンを下げましたので参考にしてみて下さい↓
世界一詳細に全部日本語でXcode11で作った新規プロジェクトをiOS12以前で実行できるところまで解説する

推奨・非推奨コード

では、これでiOS11 ~ iOS14に対応するアプリとなりました。
実装する前にAppleが推奨・非推奨しているコードを紹介していきます。

※私の記事に関係があるコードだけを紹介しますのでご了承ください。

// CLLocationManagerクラスのインスタンス初期化および、認証ステータスが変更されたら呼ばれるデリゲートメソッド

// 推奨していない iOS 4.2–14.0
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus)

// 推奨している iOS14-
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager)
// 位置情報の認証ステータス取得

// 推奨していない iOS 4.2–14.0
func authorizationStatus() -> CLAuthorizationStatus

// 推奨している iOS14-
var authorizationStatus: CLAuthorizationStatus

こんな感じに変更されています。

後は、iOS14から正確な位置精度という項目が新たに追加されました。
簡単に言うと、位置情報の精度を正確にするか曖昧にするかを自分で選択できるようになったんですね。

下記のコードのように、選択肢によって分岐することができます↓

switch locationManager.accuracyAuthorization {

      // 正確な位置情報を取得した場合  
      case .fullAccuracy:

      // 曖昧な位置情報を取得した場合 
      case .reducedAccuracy:

      default:
          break
}

ここは開発するアプリによって変わってきますので、よく吟味した方が良い部分ですね。
こちらの記事が詳しく書いていますので参考にしてみて下さい↓
iOS14でのCore Location変更点
iOS 14 でさらに強化された位置情報まわりのプライバシー

※今回は、位置情報の取得で配慮する点をまとめた記事なので
あまり正確かどうかは関係ないので紹介だけに留めておきます。

注目してほしい部分は、最初の方で説明した推奨・非推奨のコードです。
iOS14以前のバージョンに対応させるには非推奨のコードも使用しなければいけません。

では早速、実装していきます。

iOS14以前のバージョンに対応した場合の実装

※長いので一部抜粋

ViewController
import UIKit
import CoreLocation

final class ViewController: UIViewController {

    private var locationManager: CLLocationManager = {
        var locationManager = CLLocationManager()
        locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
        locationManager.distanceFilter = 5
        return locationManager
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        locationManager.requestWhenInUseAuthorization()
        locationManager.delegate = self
    }
}
    // 推奨しているデリゲートメソッド
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        // iOS14以降の場合
        if #available(iOS 14.0, *) {
            if CLLocationManager.locationServicesEnabled() {

                let status = manager.authorizationStatus

                switch status {
                case .authorizedAlways, .authorizedWhenInUse:
                    manager.startUpdatingLocation()

                case .notDetermined:
                    manager.requestWhenInUseAuthorization()

                case .denied:
                    Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                        UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                    }

                case .restricted:
                    Alert.okAlert(vc: self, title: "位置情報サービスの使用を\n許可されていません", message: "何らかの制限が掛かっています")

                default:
                    break
                }
            }else {
                Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
            }
        }
    }
    // 非推奨のデリゲートメソッド
    //iOS14以前の場合
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        if CLLocationManager.locationServicesEnabled() {

            switch status {
            case .authorizedAlways, .authorizedWhenInUse:
                manager.startUpdatingLocation()

            case .notDetermined:
                manager.requestWhenInUseAuthorization()

            case .denied:
                Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                    UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                }

            case .restricted:
                Alert.okAlert(vc: self, title: "位置情報サービスの使用を\n許可されていません", message: "")

            default:
                break
            }

        }else {
            Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
        }
    }

こんな感じの実装となり、どちらも記述します。
メソッド内は、バージョンの指定以外、ほぼ変わらないですね。

これでiOS14以前のバージョンに対応できました。

エラー処理

どの場面でも大体、エラー処理というものは存在しますが、もちろんCLLocationManagerでも存在します。
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error)

では、どのようにViewController内で記述するのかというと、こんな感じです↓

ViewController
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    //本来なら、アラートで画面にエラー内容を表示したりする
    print("\((error as NSError).domain)")
}

このデリゲートメソッドはCLLocationManagerが位置情報を取得できなかった時に呼ばれます。
このメソッドを実装していない場合、エラーをスルーしてしまうので要注意です。

様々なエラーがあるので、見てみて下さい↓
CLError

そして、ここからは自分が苦戦した所を紹介します。

苦戦した部分~認証ステータスの取得~

認証ステータスの取得も、先ほど紹介しましたが推奨・非推奨のコードがありましたね。
ここが、かなり厄介?で現在進行形で悩んでいます...。

// 位置情報の認証ステータス取得

// 推奨していない iOS 4.2–14.0
func authorizationStatus() -> CLAuthorizationStatus

// 推奨している iOS14-
var authorizationStatus: CLAuthorizationStatus

まず、この2つのコードを比較すると
推奨していない方はメソッド、推奨している方はプロパティとなっています。

ここがまず、大きな違いですね。

そして、推奨していないfunc authorizationStatus()メソッドは
アプリがバックグラウンド・フォアグラウンド状態関係なく認証ステータスを返してくれますが

今回、iOS14で新しく登場したauthorizationStatusプロパティの方は
フォアグラウンド状態の時の認証ステータスしか返してくれません。

つまり、authorizationStatusプロパティの方はバックグラウンド状態で認証ステータスを変更した場合
フォアグラウンド状態の時の認証ステータスを返すんですね。

じゃあ、この部分の一体どこで苦戦したのかというと

バックグラウンド状態から戻ってきた時にアプリの位置情報サービスの有無をチェックする部分です。

実際に分かりやすいようにコードで説明しますね。
認証ステータスを取得する場合、今までだと下記の実装でした↓

ViewController
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    }

   // バックグラウンド状態から戻った時に呼ばれるメソッド
   @objc private func willEnterForeground() {
      if CLLocationManager.locationServicesEnabled() {
         // 認証ステータスの取得(非推奨)
         let status = CLLocationManager.authorizationStatus()

         switch status {

         // 許可しない場合
         case .denied:
             Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                 UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
             }

            default:
                break
            }

        }else {
            Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
        }
      }
   }

では次に、推奨しているコードでの実装↓

ViewController
    private var locationManager: CLLocationManager = {
        var locationManager = CLLocationManager()
        locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
        locationManager.distanceFilter = 5
        return locationManager
    }()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    }

   // バックグラウンド状態から戻った時に呼ばれるメソッド
   @objc private func willEnterForeground() {
      if CLLocationManager.locationServicesEnabled() {
         // 認証ステータスの取得(推奨)
         let status = locationManager.authorizationStatus

         switch status {

         // 許可しない場合
         case .denied:
             Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                 UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
             }

            default:
                break
            }

        }else {
            Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
        }
      }
   }

さほど変化はないんですが、これをビルドしてみると違いが分かります。

まず、どちらも起動時に認証ステータスが .denidである場合はちゃんとアラートを表示します。
ですが、バックグラウンド状態で .denid > .restrictedに変更してフォアグラウンド状態に戻すとこうなります↓
ezgif.com-gif-maker4.gif

本来、許可しない(.denid)で表示されるアラートが
Appの使用中は許可(.authorizedWhenInUse)でも表示されてしまいました。

この不具合が、発生してしまうので仕方なく非推奨のfunc authorizationStatus()メソッド
を呼んでいる現状なんですね。

authorizationStatusプロパティはどう使うんだろう?

色々、試行錯誤していきながら調べながら自分はこういう結論に至りました。

そもそも、iOS14で登場したauthorizationStatusプロパティは

func locationManagerDidChangeAuthorization(_ manager: CLLocationManager)ありきで使用してこのメソッド内で使っていく。

じゃあ、そもそも非推奨のfunc authorizationStatus()メソッドは使わなくても良いんじゃない?

そのメソッドはCLLocationManagerクラスの初期化及び、バックグラウンド・フォアグラウンド状態関係なく認証ステータスが変更されたら呼ばれるデリゲートメソッドだし、willEnterForeground()は必要ないのでは?↓

// バックグラウンド時に戻った時に呼ばれるメソッド
@objc private func willEnterForeground()

と思う方がいらっしゃると思います。

では、このような場面ではどうでしょうか?

例えば、ユーザーに対して位置情報の許可をリクエストして
アラートが表示され、許可しないを選択したとします。

今までの流れからいくと、このようなアラートが表示されて設定アプリに画面遷移します。

これで、設定アプリに画面遷移したはいいけど何もしないで戻った時
func willEnterForeground()を呼ばない場合どうでしょうか?

呼ばない場合は、何もチェックされません。

もちろん、認証ステータスが変更されていないので
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager)
も呼ばれません。

以上の理由からfunc willEnterForeground()を使用していますし、
非推奨のfunc authorizationStatus()メソッドも使用している現状なんですね。

この部分の改善方法を色々、調べたんですが最新の記事でも非推奨のfunc authorizationStatus()メソッドを使用していて頭の中は???でいっぱいでした。

この解決方法をご存知の方は、コメントで教えて下さると有り難いです。

おわり

という事で三部構成という長い記事になってしまいましたが、今回で完結です。
位置情報を取得するだけで、ここまで配慮しなくてはいけないというのはびっくりですね。

もしかしたら、ここは全然違うよー!という部分があるかもしれません。
そういう場合は遠慮なく、コメントして下さると有り難いです。

※おわりで言うのもなんですが、アラートのタイトルなどで何度も同じことを書くのは
マジックナンバー扱いになってしまい駄目なのですが、これは後日、記事としてまとめて投稿します。

ここまでのソースコードは下記にて載せておきますので良かったら参考にしてみて下さい↓

最後まで読んで下さり、ありがとうございました。

※追記

extentionファイルを作って拡張すれば、ViewControllerのボリュームを抑えることも出来ます↓

CLLocationManager+
import UIKit
import CoreLocation

extension CLLocationManager {
    func alertStatusIfNeeded(vc: UIViewController) {
        if #available(iOS 14.0, *) {
            if CLLocationManager.locationServicesEnabled() {
                let status = self.authorizationStatus

                switch status {
                case .notDetermined:
                    self.requestWhenInUseAuthorization()

                case .restricted:
                    // アラート表示したり

                case .denied:
                    // アラート表示したり

                case .authorizedAlways:
                    self.startUpdatingLocation()

                case .authorizedWhenInUse:
                    self.startUpdatingLocation()

                default:
                    break
                }
            }else {
                // アラート表示したり
            }
        }
    }

    func alertStatusIfNeededUnderiOSVer(vc: UIViewController) {
        if CLLocationManager.locationServicesEnabled() {
            let status = CLLocationManager.authorizationStatus()

            switch status {
            case .notDetermined:
                self.requestWhenInUseAuthorization()

            case .restricted:
                // アラート表示したり

            case .denied:
                // アラート表示したり

            case .authorizedAlways:
                self.startUpdatingLocation()

            case .authorizedWhenInUse:
                self.startUpdatingLocation()

            default:
                break
            }
        }else {
           // アラート表示したり
    }

    func alertStatusIfNeededBackground(vc: UIViewController) {
        if !CLLocationManager.locationServicesEnabled() {
            // アラート表示したり
            return
        }

        let status = CLLocationManager.authorizationStatus()

        if status == .denied {
            // アラート表示したり
        }
    }
}

ViewControllerではこんな感じになります↓
※一部抜粋

ViewController
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        manager.alertStatusIfNeeded(vc: self)
    }

ソースコード

こちらにもあげてます。

ViewController
import UIKit
import CoreLocation

final class ViewController: UIViewController {

    private var locationManager: CLLocationManager = {
        var locationManager = CLLocationManager()
        locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
        locationManager.distanceFilter = 5
        return locationManager
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        locationManager.requestWhenInUseAuthorization()
        locationManager.delegate = self
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    @objc private func willEnterForeground() {
        if CLLocationManager.locationServicesEnabled() {

            let status = CLLocationManager.authorizationStatus()

            switch status {
            case .denied:
                Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                    UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                }

            default:
                break
            }

        }else {
            Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
        }
    }
}

extension ViewController: CLLocationManagerDelegate {
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        if #available(iOS 14.0, *) {
            if CLLocationManager.locationServicesEnabled() {

                let status = manager.authorizationStatus

                switch status {
                case .authorizedAlways, .authorizedWhenInUse:
                    manager.startUpdatingLocation()

                case .notDetermined:
                    manager.requestWhenInUseAuthorization()

                case .denied:
                    Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                        UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                    }

                case .restricted:
                    Alert.okAlert(vc: self, title: "位置情報サービスの使用を\n許可されていません", message: "")

                default:
                    break
                }
            }else {
                Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
            }
        }
    }

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        if CLLocationManager.locationServicesEnabled() {

            switch status {
            case .authorizedAlways, .authorizedWhenInUse:
                manager.startUpdatingLocation()

            case .notDetermined:
                manager.requestWhenInUseAuthorization()

            case .denied:
                Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in
                    UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
                }

            case .restricted:
                Alert.okAlert(vc: self, title: "位置情報サービスの使用を\n許可されていません", message: "")

            default:
                break
            }

        }else {
            Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let gps = manager.location?.coordinate else {
            return
        }
        manager.stopUpdatingLocation()
        let lat = gps.latitude
        let lng = gps.longitude
        print("経度:\(String(describing: lat)), 緯度:\(String(describing: lng))")
    }
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("\((error as NSError).domain)")
    }
}
Alert
import UIKit

final class Alert {
    static func okAlert(vc: UIViewController,title: String, message: String, handler: ((UIAlertAction) -> Void)? = nil) {
        let okAlertVC = UIAlertController(title: title, message: message, preferredStyle: .alert)
        okAlertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: handler))
        vc.present(okAlertVC, animated: true, completion: nil)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AppStore「自動更新サブスクリプション」の基本の基

AppStore「自動更新サブスクリプション」の基本の基

AppStore「自動更新サブスクリプション」をなんとなく構築してしまっていたので、改めて基本の各ワードについてまとめてみました

体系的にというよりは、少し理解が必要な部分についてフォーカスを置いております。

※主に自身の毎日の復習・学習の機会創出、アウトプットによる知識の定着を目的としております。
暖かい目で見ていただけますと幸いです。

そもそもAppStoreの自動更新サブスクリプションとは

※公式ページより抜粋
自動更新サブスクリプションとは、ユーザーがAppのコンテンツ、サービス、プレミアム機能を継続的に利用できるようにするものです。各サブスクリプションの期間が終了すると、ユーザーがキャンセルしない限り自動的に更新されます。ユーザーはiOS、iPadOS、macOS、watchOS、tvOS上でサブスクリプションに登録できます。

サブスクリプションを提供するAppは、ユーザーにとって価値のある、革新的なApp体験を継続的に提供することで、定期的な支払いに値するものになります。サブスクリプションモデルの導入を検討している場合は、機能強化やコンテンツの拡充で、Appを定期的にアップデートすることを計画しましょう。

公式ページ

一言で言うと、AppStroeにおける「自動更新サブスクリプション」は「毎月の月額課金を自動更新形式で構築することができる機能」です!

App手数料:サブスクリプションも通常の決済と変わらない

通常のAppStoreの手数料は30%です。
サブスクリプションにおいても同様に手数料は30%で、毎月月額の30%をAppleに手数料と納めて残りの70%をアプリ作成者は受け取ることができます。

サブスクリプションは1年経過後に手数料が安くなる!

ユーザーのサブスクリプションの利用期間が1年経過後は、そのユーザーの課金額に対する手数料が15%に割引されます!
これはユーザーごとでの換算となりますので、ユーザーごとに継続期間に合わせた手数料が課せられる形式となります。

※以下、公式ページより抜粋

自動更新サブスクリプションは、App Storeにおける他のビジネスモデルとは純収益の構造が異なります。サブスクリプション利用者の最初の1年間、デベロッパはそれぞれの請求サイクルで、サブスクリプションの価格の70%から税額を差し引いた金額を受け取ります。サブスクリプション利用者の有料サービスの日数が1年分積算されると、デベロッパの純収益率はサブスクリプションの価格の85%から税額を差し引いた金額に引き上げられます。

仕組みは次のようになっています。

・すべてのAppleプラットフォームの自動更新サブスクリプションで有効です。
・お試し価格の期間(都度払い、前払い)も利用日数に含まれます。
・無料トライアルおよびボーナス期間は利用日数に含まれません。
・同じグループ内のサブスクリプション間でアップグレード、ダウングレード、クロスグレードが行われても、有料サービスの利用日数には影響しません。
・異なるグループのサブスクリプションに切り替えた場合は、サービスの利用日数がリセットされます。

キャンセルや請求の問題などのためにサブスクリプションがアクティブでなくなった場合、有料サービスの日数は、ユーザーが60日以内に再開しない限り、85%の純収益率に必要な有料サービスの日数として加算されなくなります。

サブスクリプショングループとは

提供するサブスクリプションは全て、1つのサブスクリプショングループに割り当てる必要があります。
つまり、サブスクリプショングループは必須です。

サブスクリプショングループとは:一言で言うと、同一種類のサブスクリプションのハイエンド・ローエンドのメニューをグルーピングする(束ねる)機能です。

束ねて置くことで、グレードアップ、グレードダウンを自動で判別して対応してくれます!

※以下、公式ページより抜粋

提供するサブスクリプションはすべて、1つのサブスクリプショングループに割り当てる必要があります。サブスクリプショングループは、アクセスレベル、価格、期間が異なる複数のサブスクリプションで構成されているため、ユーザーが自分のニーズに最適なオプションを選択できるようになっています。ユーザーが1回に購入できるのはグループ内の1つのサブスクリプションのみであるため、ほとんどのAppでは、グループを1つだけ作成することがベストプラクティスです。これにより、ユーザーが複数のサブスクリプションを誤って購入してしまう事態を避けることができます。

アップグレード、ダウングレードについて

同じサブスクリプショングループ内のサブスクリプションであればAppStoreが自動で判定し、アップグレード・ダウングレードをしてくれます。

反対に、サブスクリプショングループが別の場合、アップグレードダウングレードはされません。新規のサブスクリプションと判定され、旧コース・新コースのどちらにも入ることになってしまうため注意しましょう。

アップグレード、ダウングレード時の請求について

アップグレードの場合:
・変更前のサブスクリプションのお金:即時、比例配分された金額を返金
・変更後のサブスクリプションのお金:即時、請求

ダウングレードの場合:
・変更前のサブスクリプションのお金:対応なし
・変更後のサブスクリプションのお金:次回更新時から請求
- 即時切り替えのアップグレードとは違い、次回の更新のタイミングで切り替わる形になります。

※以下、公式ページ抜粋

アップグレード

ユーザーが、現在のサブスクリプションよりもサービスレベルの高いサブスクリプションを購入することを指します。この場合、ユーザーはただちにアップグレードされ、元々のサブスクリプションの比例配分された金額が返金されます。追加のコンテンツや機能をユーザーがただちに利用できるようにしたい場合は、そのサブスクリプションを高くランク付けし、ユーザーがアップグレードとして購入できるようにしてください。

ダウングレード

ユーザーが、現在のサブスクリプションよりもサービスレベルの低いサブスクリプションを選択することを指します。この場合、現在のサブスクリプションは次回の更新日まで継続され、その後低いレベルと価格で更新されます。

クロスグレード

ユーザーが、同等のレベルのサブスクリプションに切り替えることを指します。サブスクリプションの期間が同じであれば、新しいサブスクリプションはただちに開始されます。期間が異なる場合は、新しいサブスクリプションは次回の更新日に有効になります。

レシート情報

レシートデータとは、Appleの公式の電子的な購入情報です。
レシートデータはユーザーごとに存在しており、ユーザー固有の値(※Base64エンコードしたレシート情報とか)をAppstoreサーバーにHTTPでPOSTリクエストすることでJSON形式で取得することができます。

自動更新サブスクリプションでは、レシート情報が自動更新される仕組みになっており、提供者側(配信者)はこのレシート情報をチェックすることで現在の該当ユーザーの購入情報をチェックできます。

※以下公式ページ抜粋
App Storeのレシートは、Apple証明書で署名されたバイナリ形式の暗号化ファイルです

公式ページ App Storeを使用したレシートの検証

サーバー通知

サーバーサイドのURLをAppStoreConnectに登録しておくことで、自動更新サブスクリプションのステータスが変更したときにAppleから通知を取得することができます。

この機能があることで、入会・解約・アップグレード・ダウングレードなども配信者側はオンタイムで確認・知ることができます。

なぜサーバー通知が必要か

自動更新サブスクリプションをユーザーとして利用したことがある人はわかるかと思いますが、ユーザーは入会はアプリ側で実施することができますが解約はアプリ側ではできず必ず設定画面(あのグレーのアイコンのやつ)で行わないといけません。

このことで何が起きるかと言うと、
入会はアプリ上で行われるので、アプリ上で入会したことを知ることができそれに合わせた処理をすることができます。
ですが、退会はiPhoneの設定機能で行われるため、アプリ側ではそれを知ることができず退会などに合わせた処理を行うことができないのです。その結果、退会・アップグレードしたのにアプリ側のデータ・状態は退会前・アップグレード前のままになってしまいます。

サンドボックス

サンドボックス環境 = テスト環境です

課金のテストやサーバー通知、レシート情報関連の処理をテスト環境でテストすることができます

まとめ

・AppStoreの自動更新サブスクリプションは月額課金制度を導入できる
・それに関連する主要な機能がいくつもあり、それぞれを理解して使う必要がある

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む