- 投稿日:2021-01-12T23:01:22+09:00
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.値に合わせて、必要な処理を実装しましょう
- 投稿日:2021-01-12T22:42:50+09:00
iOSアプリで位置情報を取得するときに配慮する点をまとめてみた③
前回の続き↓
iOSアプリで位置情報を取得するときに配慮する点をまとめてみた②はじめに
iOSアプリで位置情報を取得するときに配慮する点をまとめた内容となっています。
前回までのあらすじ
前回、
アプリの位置情報サービスの有無
をバックグラウンド状態
から戻った時も
チェックをしなければいけないと書きました。アプリが
バックグラウンド状態
から戻った時にある処理をするにはAppDelegate
のapplicationWillEnterForeground(_ application: UIApplication)
で処理するんでしたね。そして、アラートを表示したいのでViewController内で処理するために
NotificationCenter
を使いました。バックグラウンド状態から戻ったときにチェックする(アプリ)
では、実際にコードを書いてみます。
※長くなるので前回のソースコードから一部抜粋今回も、アラートの表示はこの方法を用います↓
UIAlertControllerをファイルを分けて実装してみるViewControllerimport 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: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } }端末の位置情報サービスがオンかオフかを分岐して
オン
の場合にアプリの位置情報サービスの有無
をチェックしています。ビルドしてみると、ちゃんとチェックされてますね↓
※バグ?のせいなのか、たまに位置情報サービスの認証ステータスが
次回確認
になっていますが
本来はなし
になります。
因みに、この部分は.denied
だけのケースのみでOKです。理由は認証ステータスが変更されれば
デリゲートメソッド
が呼ばれますし、設定アプリ
に画面遷移するのは.denied
の場合のみで、その時に何もしないで戻った場合をチェックしたいので他の認証ステータスの記述は必要ないです。ここで、一旦、全体のソースコードを見てみましょう。
ViewControllerimport 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))") } }Alertimport 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
にしていましたが、アラートなどでユーザーに伝えたほうがいいので改修しときましょう。ViewControllercase .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以前のバージョンに対応した場合の実装
※長いので一部抜粋
ViewControllerimport 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内で記述するのかというと、こんな感じです↓
ViewControllerfunc 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
プロパティの方はバックグラウンド状態
で認証ステータスを変更した場合
フォアグラウンド状態
の時の認証ステータスを返すんですね。じゃあ、この部分の一体どこで苦戦したのかというと
バックグラウンド状態
から戻ってきた時にアプリの位置情報サービスの有無
をチェックする部分です。実際に分かりやすいようにコードで説明しますね。
認証ステータス
を取得する場合、今までだと下記の実装でした↓ViewControlleroverride 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: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } } }では次に、
推奨
しているコードでの実装↓ViewControllerprivate 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に変更してフォアグラウンド状態
に戻すとこうなります↓
本来、
許可しない(.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ではこんな感じになります↓
※一部抜粋ViewControllerfunc locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { manager.alertStatusIfNeeded(vc: self) }ソースコード
こちらにもあげてます。
ViewControllerimport 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)") } }Alertimport 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) } }
- 投稿日:2021-01-12T00:39:17+09:00
SwiftUIでもSize Classで縦横のレイアウトを切り替える
はじめに
今さらながら、初めてSwiftUIで新規アプリを開発する機会があり、
縦向きと横向きでレイアウトを変える必要があったのでその方法について書きます(`・ω・´)サイズクラスについて
画面サイズは、高さと幅(height and width)とサイズ(regular and compact)の組み合わせで定義されます。
ざっくりと以下のような感じです。
width height デバイスの種類と向き regular regular iPadの縦向き・横向き compact regular iPhoneの縦向き regular compact 大きいiPhone(Plus / Max / XR and 11など)の横向き compact compact iPhoneの横向き ここでは詳細は割愛しますが、HIGのAdaptivity and Layout の「Size Classes」のところに詳細書いてあります( ̄・ω・ ̄)
SwiftUIのhorizontalSizeClass, verticalSizeClassの定義について
以下のようにEnvironmentValuesにあらかじめ定義されている
horizontalSizeClass, verticalSizeClassを使用することでサイズクラスを取得できます。SwiftUI@available(iOS 13.0, *) @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) extension EnvironmentValues { /// The horizontal size class of this environment. @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) public var horizontalSizeClass: UserInterfaceSizeClass? /// The vertical size class of this environment. @available(macOS, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) public var verticalSizeClass: UserInterfaceSizeClass? }↑ UserInterfaceSizeClassは、
compact
とregular
のケースを持つenumです。SwiftUIのViewでサイズクラスを考慮してレイアウトを変更する
ここからが本題です( ̄ω ̄)
サイズクラスを取得して、iPhoneで横向きの場合はレイアウトを変えてみます。まずは、何も考慮しない場合の画面
ボタン3つとイメージ1つ、その下にまたボタンが1つある以下にもサンプルな画面です。
(いらすとやにONE PIECEのイラストが追加されていて驚きました...!)ContentView.swiftimport SwiftUI struct ContentView: View { var body: some View { VStack { VStack(alignment: .center, spacing: 16.0, content: { self.roundedButton(title: "ぼたん1", backgroundColor: .red) { print("ぼたん1タップ!") } self.roundedButton(title: "ぼたん2", backgroundColor: .yellow) { print("ぼたん2タップ!") } self.roundedButton(title: "ぼたん3", backgroundColor: .blue) { print("ぼたん3タップ!") } }) .padding([.top], 16.0) .padding([.leading, .trailing], 32.0) Spacer() Image("enel") .resizable() .frame(minWidth: 200, minHeight: 200) .aspectRatio(contentMode: .fit) Spacer() self.roundedButton(title: "ぼとむぼたん", backgroundColor: .orange) { print("ぼとむぼたんタップ!") } .padding([.top], 0) .padding([.leading, .bottom, .trailing], 32.0) } } } private extension ContentView { /// 角丸のボタン func roundedButton(title: String, textColor: Color = .white, backgroundColor: Color, action: @escaping() -> Void) -> some View { Button(action: { action() }, label: { Text(title) .frame(maxWidth: 440.0, minHeight: 44.0) .font(Font.subheadline.weight(.bold)) .foregroundColor(textColor) .background(backgroundColor) .cornerRadius(8.0) }) } }
縦向き 横向き エネルのイメージの最低サイズが決まっているので、上下のボタンが見切れてしまいました(´・ω・`)
サイズクラスを考慮して、横向きの画面のレイアウトを変更する
horizontalSizeClass, verticalSizeClassを取得するために、SwiftUIのViewにEnvironmentを追加します。
ContentView.swiftimport SwiftUI struct ContentView: View { @Environment(\.horizontalSizeClass) var hSizeClass // 追加1 @Environment(\.verticalSizeClass) var vSizeClass // 追加2 var body: some View { // ... } }毎回if文を作るのはしんどそうなので、horizontalSizeClassとverticalSizeClassでイニシャライズするenumを定義します
DeviceTraitStatus.swiftimport SwiftUI enum DeviceTraitStatus { case wRhR case wChR case wRhC case wChC init(hSizeClass: UserInterfaceSizeClass?, vSizeClass: UserInterfaceSizeClass?) { switch (hSizeClass, vSizeClass) { case (.regular, .regular): self = .wRhR case (.compact, .regular): self = .wChR case (.regular, .compact): self = .wRhC case (.compact, .compact): self = .wChC default: self = .wChR } } }先ほどの画面を修正していきます。
横向きの場合は、赤いボタンと黄色いボタンを横並びにして画面に収まるようにしてみます!ContentView.swiftimport SwiftUI struct ContentView: View { @Environment(\.horizontalSizeClass) var hSizeClass @Environment(\.verticalSizeClass) var vSizeClass var body: some View { VStack { let deviceTraitStatus = DeviceTraitStatus(hSizeClass: self.hSizeClass, vSizeClass: self.vSizeClass) switch deviceTraitStatus { case .wRhR, .wChR: self.buttonsOnPortrait case .wRhC, .wChC: self.buttonsOnLandscape } // ... } } } private extension ContentView { var button1: some View { self.roundedButton(title: "ぼたん1", backgroundColor: .red) { print("ぼたん1タップ!") } } var button2: some View { self.roundedButton(title: "ぼたん2", backgroundColor: .yellow) { print("ぼたん2タップ!") } } var button3: some View { self.roundedButton(title: "ぼたん3", backgroundColor: .blue) { print("ぼたん3タップ!") } } /// iPhoneの縦表示またはiPadの場合のボタン3つのレイアウト /// /// - Note: iPadの場合は、縦横ともに画面に収まるのでレイアウト変えない。 var buttonsOnPortrait: some View { VStack(alignment: .center, spacing: 16.0, content: { self.button1 self.button2 self.button3 }) .padding([.top], 16.0) .padding([.leading, .trailing], 32.0) } /// iPhoneの横表示の場合のボタン3つのレイアウト var buttonsOnLandscape: some View { VStack { HStack { self.button1 Spacer() self.button2 } .padding([.leading, .trailing], 32.0) .padding([.bottom], 4.0) self.button3 } .padding([.top], 16.0) } /// 角丸のボタン func roundedButton(title: String, textColor: Color = .white, backgroundColor: Color, action: @escaping() -> Void) -> some View { // ... } }
修正後の縦向き 修正後の横向き 画面回転したら、レイアウトが変更されるようになりました\(。・ω・。)/
さいごに
SwiftUIはまだまだ勉強中なので、もっと良いやり方などあればコメントをお願いしますm(_ _)m
ソースコードは、GitHubにリポジトリを作ってpushしました。
- 投稿日:2021-01-12T00:28:57+09:00
ショートカットを利用したAmazon Musicのプレイリスト自動再生
プロローグ
iPhoneのショートカットの音楽再生および操作はデフォルトの音楽アプリ"ミュージック"のみしか対応していない。そのため、今主流となりつつあるSpotify・LINE music・Apple Musicなどのストリーミングサービスやサードパーティの音楽アプリを操作することができるないので不便を感じていた。この記事にたどり着いた皆さんもきっとそう感じているに違いない。
Bluetoothイヤホン接続からの自動化を完成させたい
特に不便を感じていたのは自作の音楽自動再生ショートカットである。Bluetoothイヤホンの接続をトリガーとしてAmazon Musicを開くというもので、目標はプレイリストの再生まで持っていきたい。しかし、前述の通りアプリの操作ができないため、アプリを開くところ止まりで音楽の再生まで不可能であった。が、最近Amazon Musicにおいて、プレイリストの再生まで持っていく方法が判明した。
ショートカットを作成する
1. プレイリストの共有リンクを取得する
Amazon Musicで作成したプレイリストの詳細からプレイリスト共有用のリンクを取得する。
プレイリスト右側の… > シェアする > リンクをコピー
2. リンクをいじる
ここが肝となる工程。
取得したリンクはこのような構造になっている。
https://music.amazon.co.jp/user-playlists/@@@@@@@@@@?ref=dm_sh_~~~~~~~このリンクの
?
以降は不要なので全て削除する。そして、
?
に続いてdo=play
を書き込む。
https://music.amazon.co.jp/user-playlists/@@@@@@@@@@?do=playこれで準備完了。先ほどの未加工のリンクを開いた場合、Amazon Musicのアプリが開かれ、プレイリストの画面に到達するだけで終わる。しかし、このリンクを叩けば、プレイリストのページが開かれるのと同時に楽曲の再生が行われる。
ただし、ここでリンクを開くときのブラウザーに制限がある。
私のiPhone12の環境ではChrome, Safari, Openerの3種類を試したが、結論から言うとOpenerがベスト。
まず、Chromeだがこれは微妙。Chrome上でプレイリストが開かれ、"Amazon Musicで再生する"と表示されるだけなのでタプしなければいけない手間が増えるのと、自動で再生されないという2つの弱点がある。
次にSafiri。これは自動でAmazon Musicのアプリを開きプレイリストを再生するところまではうまく行った。しかし、Amazon Musicを既に使用中である場合、現在の曲の再生を中止してプレイリストの曲を強制的に再生させることができない。
最後にOpener。これはOpener内の設定でAmazon MusicアプリにURLを渡すように自動設定を組んでやると、URLを叩いてからノータッチでプレイリストの自動再生ができる。さらにSafariにはできなかったプレイリストの強制再生ができるのでこちらの方をお勧めする。2. ショートカットを組む
2-1. OpenerでURLを開く
作成したリンクをOpenerで開くショートカットを単一で作成しておく。
2-2. オートメーションで自動化する
オートメーションで使用しているBluetoothイヤホンが接続されたら、先ほど作成されたショートカットが起動するように設定する。
3. 動作確認
OpenerでAmazonMusicのリンクを開くがタップされているように見えるが、これはアプリ側で自動的に行われているものである。Bluetoothイヤホンを起動させてから音楽再生までに行う操作は、オートメーションの実行許可のタップのみである。