- 投稿日:2021-01-09T22:59:16+09:00
【Swift】超シンプルな課金の実装
難しいと感じる課金の実装をシンプルにまとめてみました。
コピペして実装して頂くととっても簡単にできます。必要に応じて適宜カスタムしつつ、活用してもらえたら嬉しいです。実装全体
解説
class StoreManager: NSObject, SKPaymentTransactionObserver { static var sharedStore = StoreManager() var products: [SKProduct] = [] // product idの一覧を定義する let productsIdentifiers: Set<String> = [] // AppDelegateや課金処理前に呼び出してproduct一覧を取得する static func setup() { sharedStore.validateProductsIdentifiersWithRequest() } // product情報をStoreから取得 private func validateProductsIdentifiersWithRequest() { let request = SKProductsRequest(productIdentifiers: productsIdentifiers) request.delegate = self request.start() } ... }Product情報の取得
- App Store Connectで作成した課金アイテムの
製品ID
を定義します。// productIdの一覧を定義する // 製品IDがsample.idの場合 let productsIdentifiers: Set<String> = ["sample.id"]
- 定義した
productsIdentifiers
を元にProduct情報の一覧を取得します。// product情報をStoreから取得 private func validateProductsIdentifiersWithRequest() { // 取得のリクエスト let request = SKProductsRequest(productIdentifiers: productsIdentifiers) request.delegate = self request.start() } // 取得処理の結果は`SKProductsRequestDelegate`に通知される extension StoreManager: SKProductsRequestDelegate { // product情報の取得完了 func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { products = response.products } }購入リクエスト
// 購入 func purchaseProduct(_ productIdentifier: String) { // productIdentifierに該当するproduct情報があるかチェック guard let product = productForIdentifiers(productIdentifier) else { return } // 購入リクエスト let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) } // 該当のproduct情報はproductsに存在するか確認 private func productForIdentifiers(_ productIdentifier: String) -> SKProduct? { return products.filter({ (product: SKProduct) -> Bool in return product.productIdentifier == productIdentifier }).first }購入リクエストのTransaction
// transactionsが変わるたびに呼ばれる // Transactionの状態により処理したい内容を記述する // トランザクションを終了すると消耗型のレシートは消失し、そのTransactionは復元できないので注意 func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch transaction.transactionState { case .purchasing: // キューに追加された case .purchased: // 購入が完了 case .restored: // 購入履歴から復元が完了 case .deferred: // 購入処理は保留されており、承認まち case .failed: // キューに追加される前にリクエストが失敗 default: break } } }Transactionとレシートについて
課金が完了するとAppleのサーバーにレシートが生成されます。
レシートを検証する場合、Transactionを閉じてしまうと、消耗型のレシートは削除され所得できなくなるので注意が必要です。
サブスクのレシートなどは、Transactionを閉じてもずっと取得できるので、レシートを検証してデータを作成する場合は多重に作成してしまわないように注意してください。.deferredとは?
「Ask to Buy」が有効になっているアカウントで購入しようとしたとき、購入が保留され管理者に許可を求める通知が届きます。その通知が許可されるまでTransactionの状態は
.deferred
となっています。
これはファミリー共有の機能によるものです。関連記事
- 投稿日:2021-01-09T22:18:08+09:00
[Xcode]README.mdファイルの追加方法
表題の通りXcodeにREADME.mdファイルを追加する方法について投稿します。
検索をかけたところ最近の記事にありつけなかったので、ぜひ2021年最新版として参考にしていただければと思います!
追加手順(シンプル)
Xcodeナビゲーションバー(Macディスプレイ最上部のメニューバー)ののFileを選択
↓
Newを選択
↓
Fileを選択
↓
すると画像のようにファイルを選択するメニューが出てくるので、Markdown Fileを選択してNext
↓
そのままページが切り替わるのでCreateを選択これでGitにコミットすればGithubのリポジトリでREADMEに加えた編集内容を見ることができます。
- 投稿日:2021-01-09T20:32:16+09:00
iOSアプリで位置情報を取得するときに配慮する点をまとめてみた②
前回の続き↓
iOSアプリで位置情報を取得するときに配慮する点をまとめてみた①はじめに
前回の記事を読んでない方に、簡単に説明するとiOSアプリで位置情報を取得するときに
配慮する点をまとめた内容となっています。前回までのあらすじ
前回、
端末の位置情報サービスの有無
のチェックは
アプリをインストール後の初回起動のみ
しかされないと書きました。初回起動時に位置情報サービスが
オフ
になっていたらこんなアラートを表示します↓
このチェックは
初回起動のみ
なので、2回目以降の起動時はアラートを表示するように改修しましたね↓
今回は、バックグラウンド状態
からフォアグラウンド状態
にアプリを切り替えた時にも
端末の位置情報サービスの有無
をチェックするようにしていきましょう。バックグラウンド状態から戻った時にチェックする(端末)
アプリが、
バックグラウンド状態
からフォアグラウンド状態
に切り替わった時に
端末の位置情報サービスの有無
をチェックするように処理するには
AppDelegate
のapplicationWillEnterForeground(_ application: UIApplication)にて行います。※
AppDelegate
の呼ばれる順番は、こちらの記事が非常に分かりやすかったので載せておきます↓
iOSアプリのライフサイクルAppDelegate.swift// アプリがフォアグラウンド状態に入ろうとしている時に呼ばれるメソッド func applicationWillEnterForeground(_ application: UIApplication)ですが、アラートを表示したいのでViewControllerのクラス内で処理を行いたいですね。
アプリの状態が変わったことを
AppDelegate
以外で感知して
ある処理を行いたい場合は、NotificationCenterを使用します。※こちらの記事を参考にして実装しました↓
[Swift3.0] NotificationCenter を使ってアプリの状態に応じた処理を行うViewController// ビューが表示される直前に呼ばれるメソッド // 初回表示以外にもバックグラウンド復帰、タブ切り替えなどにも呼ばれる // まだビューが表示されていないため、計算コストの高い処理は避ける override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } @objc private func willEnterForeground() { // バックグラウンド状態から戻ってきた時に端末の位置情報サービスがオフの場合 if !CLLocationManager.locationServicesEnabled() { // アラート表示 Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } }ビルドしてみると、
バックグラウンド状態
から戻った時にもアラートが表示されるようになりました↓
これで、端末の位置情報サービスの有無を起動時
とバックグラウンド状態から戻った時
に
チェックするようになりましたね。後、チェックしなければいけないのは
アプリの位置情報サービスの有無
です。アプリの位置情報サービスの有無をチェックする
位置情報を取得する際には、ユーザーに許可を貰わなければ取得出来ません。
なので、アプリはこのようなアラートを表示してユーザーに許可をリクエストします↓
ですが、もし、ユーザーが許可しない
を選択したらどうでしょうか?
位置情報が必要なアプリの場合、許可を貰わなければ困りますよね。なのでユーザーが、
許可しない
を選択した場合にアラートなどを表示したりして
アプリの位置情報取得の許可
を促さなければいけません。前回のソースコードを例に説明します。
※ちょっと長いので一部抜粋今回も、アラートの表示はこちらの方法で表示します↓
UIAlertControllerをファイルを分けて実装してみるViewControllerimport UIKit import CoreLocation override func viewDidLoad() { super.viewDidLoad() locationManager.requestWhenInUseAuthorization() locationManager.delegate = self } extension ViewController: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus switch status { case .authorizedAlways, .authorizedWhenInUse: manager.startUpdatingLocation() case .notDetermined: manager.requestWhenInUseAuthorization() // 許可しない場合 case .denied: // アラートを表示して、アプリの位置情報サービスをオンにするように促す Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "") case .restricted: break default: break } } }
許可しない
を選択した場合、アラートがちゃんと表示されました。
これでも、ちゃんとユーザーに対して
アプリの位置情報取得の許可
を促していますが
ここで、もう少し配慮してアラートのOK
を選択したら設定アプリ
に画面遷移するようにしましょう。設定アプリに画面遷移する
設定アプリ
に画面遷移するメリットとしてアプリの位置情報取得の許可
がスムーズになり
ユーザーの手間が省かれることです。画面遷移する方法は、こちらの記事が分かりやすかったので載せておきます↓
Swift5を使ってURLスキームで設定画面に遷移する方法この方法を用いて、先ほどのソースコードに追加していきます。
ViewControllerimport UIKit import CoreLocation override func viewDidLoad() { super.viewDidLoad() locationManager.requestWhenInUseAuthorization() locationManager.delegate = self } extension ViewController: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus switch status { case .authorizedAlways, .authorizedWhenInUse: manager.startUpdatingLocation() case .notDetermined: manager.requestWhenInUseAuthorization() // 許可しない場合 case .denied: // アラートを表示して、アプリの位置情報サービスをオンにするように促す // ユーザーに対して分かりやすようにmessageで、OKを選択すると設定アプリに画面遷移することを伝える Alert.okAlert(vc: self, title: "アプリの位置情報サービスを\nオンにして下さい", message: "OKをタップすると設定アプリに移動します") { (_) in // OKを選択した後、設定アプリに画面遷移する UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) } case .restricted: break default: break } } }こちらをビルドしてみると、ちゃんと
設定アプリ
に画面遷移しました。
ですが、ここでも落とし穴があります。
この方法で、設定アプリに移動したのはいいけど
アプリの位置情報取得の許可
をしないまま戻ったとしましょう。このままだと、アプリは何もチェックしません。
この場面でも、アプリが
バックグラウンド状態
から戻った時に
アプリの位置情報取得の有無
をチェックしなければいけません。またまた記事が長くなりそうなので、今回はここら辺で終わります。
ここ間違っているよー!というのがありましたら、気軽にコメントして下さい。最後まで読んで下さって、ありがとうございます!
ここまでのソースコードを下に載せておきます↓ソースコード
ViewControllerimport UIKit import CoreLocation class ViewController: UIViewController { var locationManager: CLLocationManager = { var locationManager = CLLocationManager() locationManager.distanceFilter = 5 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) } @objc private func willEnterForeground() { if !CLLocationManager.locationServicesEnabled() { Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !CLLocationManager.locationServicesEnabled() { Alert.okAlert(vc: self, title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } } } extension ViewController: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { 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 } } 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) } }
- 投稿日:2021-01-09T17:51:30+09:00
modal遷移をフルスクリーンで、下から出ないように行う
全画面で遷移したいとき、下から出したくないとき、ありますよね。
以下はUIButton
にaddTarget
し、遷移する関数です。@objc func buttonTapped() { let nextVC = nextViewController() nextVC.modalTransitionStyle = UIModalTransitionStyle.crossDissolve nextVC.modalPresentationStyle = UIModalPresentationStyle.fullScreen self.navigationController?.pushViewController(nextVC, animated: true) }
UIModalTransitionStyle
は列挙型で、は遷移アニメーションを指定することができます。public enum UIModalTransitionStyle : Int { case coverVertical = 0 case flipHorizontal = 1 case crossDissolve = 2 @available(iOS 3.2, *) case partialCurl = 3 }デフォルトでは
coverVertical
が設定されているので、自然に遷移する(?)crossDissolve
に変えます。同様に
modalPresentationStyle
も列挙型の中からfullScreen
に変更します。ちなみに
modalPresentationStyle
は以下のように書かれています。public enum UIModalPresentationStyle : Int { case fullScreen = 0 @available(iOS 3.2, *) case pageSheet = 1 @available(iOS 3.2, *) case formSheet = 2 @available(iOS 3.2, *) case currentContext = 3 @available(iOS 7.0, *) case custom = 4 @available(iOS 8.0, *) case overFullScreen = 5 @available(iOS 8.0, *) case overCurrentContext = 6 @available(iOS 8.0, *) case popover = 7 @available(iOS 7.0, *) case none = -1 @available(iOS 13.0, *) case automatic = -2 }
- 投稿日:2021-01-09T17:30:24+09:00
UnsafePointer相関図
% swift --version Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28) Target: x86_64-apple-darwin20.2.0書いたこと
- UnsafePointer系クラスの関係
- UnsafePointer
- UnsafeMutablePointr
- UnsafeBufferPointer
- UnsafeMutableBufferPointer
- UnsafeRawPointer
- UnsafeRawMutablePointer
- UnsafRawBufferPointer
- UnsafeRawMutableBufferPointer
- UnsafePointerへの暗黙的変換
- Rawポインタから非Rawポインタへの接続
- Unsafeの意味
UnsafePointer相関図
UnsafePointerには数多くのクラスが用意されていますが、immutableなUnsafePointerワールドとmutableなUnsafeMutablePointerの2つの世界に分けることができます。
UnsafePointerワールド
UnsafeMutablePointerワールド
immutableとmutableの違い
値自体には、特段の違いはありません。
また相互に初期化メソッドを通して変換も可能です。ただmutable系はstatic関数として、
allocate
メソッドが用意されています。UnsafePointerの生成.swiftfunc makePointer<T>(withVal val: T) -> UnsafePointer<T> { let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1) // Tインスタンスを一つ作成する pointer.initialize(to: val) // 必ず初期化する return UnsafePointer(pointer) // UnsafePointer<T>に変換 } typealias Couple<T> = (T, T) let pointer = makePointer(withVal: Couple(1, 2)) print(pointer.pointee)自身でallocateでUnsafePointerを生成すると、deallocateを呼び出さなければメモリーリークになります。
UnsafePointerへの暗黙的変換 (Implicitly bridging)
関数の実引数に
&
演算子をつけて渡すことで、暗黙的にUnsafePointer,UnsafeRawPointerに変換されます。
Arrayの場合は、&
も不要です。
また関数内のみで有効なUnsafePointerになりますので、dellocateを呼び出す必要がありません。UnsafePointerへの変換は暗黙的変換によるキャストを使うと楽.swiftfunc print<T>(atAddress pointer: UnsafePointer<T>) { print(pointer.pointee) } // 暗黙的変換でわたせるのはミュータブルな変数のみ var num = 5 print(atAddress: &num) // numは`UnsafePointer<Int>`にキャストされる var hearts = ["?", "?", "?", "?"] print(atAddress: &hearts) // heartsは`UnsafePointer<String>` にキャストされる printStrings(atAddress: hearts) // 配列は&演算子が不要 /* 結果 5 ? ? */&演算子によるるキャストは関数の実引数に渡すときのみ有効です。
&演算子による暗黙的変換は実引数を渡すときのみ可能.swift// NG var num = 5 let pointer: UnsafePointer<Int> = &num /* コンパイルエラー error: use of extraneous '&' let pointer2: UnsafePointer<Int> = &num ^ */Unsafe Buffer Pointer系のクラスは暗黙的変換ができません。
UnsafeBufferPointer系は暗黙的変換によるキャストができない.swift// NG func printStrings(atAddress: UnsafeBufferPointer<String>) {} var feelings = ["?", "?", "?", "?"] printStrings(atAddress: &feelings)またUnsafePointer系のイニシャライザにおいて、
&
演算子による暗黙的変換は動作未定とされていますので、NGです。UnsafePointerイニシャライザでの暗黙的変換は動作未定義.swiftlet pointer = UnsafePointer(&hearts) /* ダングリングポインタになりますという警告が表示される warning: initialization of 'UnsafePointer<String>' results in a dangling pointer let pointer = UnsafePointer(&hearts) */NOTE:
- ダングリングポインタ: 確保したメモリが宙に浮くこと、詳細はUnsafeの意味に記載
UnsafeRaw(Mutable)PointerからUnsafe(Mutable)Pointer<Pointee>への接続
Unsafe Raw Pointer、Unsafe Raw MutablePointerは、特定の型を指し示さないVoidポインタ型を表しています。
そのためこれらを特定の型を指し示すUnsafePointer<Pointee>、UnsafeMutablePointer<Pointee>に接続するには、bindする必要があります。
bindは、以下のメソッドを通して行います。bindMemory(to:capacity:)UnsafeRawPointerからUnsafePointer<Pointee>へのバインド
UnsafePointerにbindする.swiftfunc printInt(_ rawPointer: UnsafeRawPointer) { // UnsafePointer<Int>に変換 let pointer = rawPointer.bindMemory(to: Int.self, capacity: 1) print(pointer.pointee) } var num = [1, 2, 3, 4, 5] printInt(&num)Unsafeの意味
Unsafeという名前通り、このクラスを使う時、以下のことに注意が必要です。
(特にCライブラリからグローバルな値を受け取る時に注意です。いつその中身が開放されているとかわからないですから。。。)
- メモリーリーク
- ダングリングポインタ (指し示す値が開放されている)
- 範囲外へのアクセス
メモリーリーク
Mutable系のクラスは
allocate
メソッドが用意され、直接UnsafePointerを作ることができます。
ただしallocateした中身は自身が責任を持ってdeallocate
する必要があります。メモリーリーク.swiftstruct Person { let name: String let age: Int } func main() { let tanaka = UnsafeMutablePointer<Person>.allocate(capacity: 1) tanaka.initialize(to: Person(name: "Tanaka", age: 24)) print(tanaka.pointee) // deallocateを呼び出さなければメモリリーク tanaka.deallocate() } main()ダングリングポインタ
UnsafePointer系クラスは、すでに
deallocate
が呼び出されている可能性があります。例1 deallocateを呼び出されたUnsafePointerを保持してしまっている
ダングリングポインタ.swiftstruct Person { let name: String let age: Int } var tanakaPointer: UnsafePointer<Person>? func main() { let tanaka = UnsafeMutablePointer<Person>.allocate(capacity: 1) tanaka.initialize(to: Person(name: "Tanaka", age: 24)) print(tanaka.pointee) // deallocを呼び出さなければメモリリーク tanaka.deallocate() // 中身が開放されているので、ダングリングポインタとなる tanakaPointer = UnsafePointer(tanaka) } main() if let pointer = tanakaPointer { // 動作未定義 print(pointer.pointee) }例2 Cライブラリ側でダングリングポインタが発生
Cライブラリ側でダングリングポインタが発生する可能性.swiftfunc callDoSomething() { let num = 5 doSomething(&num) // この関数が終わるとnumは廃棄される } func doSomething(_val: UnsafePointer<Int>) { // cライブラリ上の関数に渡す call_c_func(val) }上記、cライブラリ上でvalを持ち回してしまった場合、そのポインタ変数はダングリングポインタとなってしまっています。
範囲外へのアクセス
UnsafePointer、UnsafeRawPointerは、
+
演算子によって次のポインタにアクセスが可能ですが、
範囲を超えるとクラッシュします。範囲外へのアクセスはクラッシュする.swiftfunc itelatePointer<T>(_ pointer: UnsafePointer<T>, count: Int) { for i in 0 ..< count { // +演算子で次のポインタにアクセスが出来る print((pointer + i).pointee) } } var feelings = ["?", "?", "?", "?"] itelatePointer(feelings, count: 4) // OK // itelatePointer(feelings, count: 5) // NG クラッシュするまとめ
Cライブラリの関数において、引数にポインタ型が定義されている場合、Swift側では、UnsafePointerとしてやりとりする必要があります。
また一部Fondationクラスには、UnsafePointerを受け取るメソッドが用意されています。UnsafePointer型の場合は、中身が開放されている可能性があるため、受け取った場合は、さっさとpointeeで値を取り出し、
渡す場合は、値の中身が開放されないように、責任を持ってallocate
、dealloc
で管理しましょう。つまり危険です。
- 投稿日:2021-01-09T13:10:47+09:00
uitabbarcontroller reload after login with FirebaseAuth
追記。書きましたがログイン済みユーザーの場合、Observe(Listen)が2回走ってしまう(ViewControllerのviewDidLoadが2回呼ばれるため)のでおかしくなりますし、やはりListenはviewWillAppearでやるのが正しいみたいです。
https://firebase.googleblog.com/2015/10/best-practices-for-ios-uiviewcontroller_6.htmlただタブ遷移で毎回Listenとremoveを繰り返してしまいますので、これを辞めたい場合は別の方法で回避する必要がありそうです。
一応しばらく残して置きますが、後ほど消すか書き直します。
前提
タブバーのあるアプリでFirebaseAuthを使ったログインを実装。Initial View をMyTabBarControllerにしている。ホームとしてMyTabBarControllerの最初のViewをViewControllerにしている。
もしユーザー認証されていなかったら、LoginView/SignUpViewをPresentModallyで開いて、ログイン完了したらdissmissで閉じる仕様(その方がログイン済みユーザーにとっては余計なparentや処理がないかなと思って)。
viewのライフサイクルのおさらい
ログイン後にホームのcollectionViewが更新されない
Initial View にしているMyTabBarControllerと、その子のViewControllerのviewDidLoad, viewDidAppearは最初に呼ばれる。ログイン後にdissmissしても、子であるViewControllerのviewDidLoadやviewWillAppear, viewDidAppearは呼ばれない。そこでログインしたらMyTabBarControllerか、その子をリロード(初期化)してやる必要があった。
解決方法
dismiss後のMyTabBarControllerのviewDidAppearにて最初の子供だけリロードしてやった。
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard let _ = Auth.auth().currentUser else { self.presentWelcomeView() return } self.viewControllers?.first?.loadView() self.viewControllers?.first?.viewDidLoad() }全体のコード
MyTabBarController.swiftimport UIKit import FirebaseAuth class MyTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard let _ = Auth.auth().currentUser else { self.presentWelcomeView() // ここで呼ばないと whose view is not in the window hierarchy! のエラーが出て怒られる(viewDidLoadではViewはロードされただけで生成はされてないかららしい) return } // self.setViewControllers(self.viewControllers, animated: true) //これではダメ self.viewControllers?.first?.loadView() self.viewControllers?.first?.viewDidLoad() } func presentWelcomeView() { guard let controller = storyboard?.instantiateViewController(identifier: "WelcomeNavigation") else { return } controller.modalPresentationStyle = .fullScreen self.present(controller, animated: true, completion: nil) } }ViewController.swiftimport UIKit import FirebaseAuth import FirebaseFirestore class ViewController: UIViewController { @IBOutlet weak var collectionView: UICollectionView! var posts: [Post] = [] var user: User! let db = Firestore.firestore() var userRef: DocumentReference? = nil var listener: ListenerRegistration? override func viewDidLoad() { super.viewDidLoad() guard let user = Auth.auth().currentUser else { return } self.user = user self.userRef = db.collection("users").document(self.user.uid) startObservingDatabase() // 省略:addSnapshotListenerによるリッスン処理 self.collectionView.delegate = self self.collectionView.dataSource = self } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 公式ではここでAuth.auth().currentUserを読んでいる // しかしここはタブを押すたびに呼ばれるので毎回collectionViewが最新に更新されて本意ではない } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.listener?.remove() } }LoginViewController.swiftimport UIKit import FirebaseAuth class LoginViewController: UIViewController { func signIn() { //if let tabvc = self.navigationController?.parent as? UITabBarController { // ここがそもそもnilでダメ // tabvc.viewControllers?.first?.loadView() // tabvc.viewControllers?.first?.viewDidLoad() //} self.navigationController?.dismiss(animated: true, completion: nil) // 今回はたまたま親にNavigationControllerを持たせていたので、ないならself.dismissでもOKなはず } }参考
- 投稿日:2021-01-09T11:38:18+09:00
TableViewを余分にスクロールできるようにする
はじめに
TableViewの上にボタンを重ねるUIを作る時に、一番下にスクロールしてもボタンに重なったままになるのを防ぐために余分なスクロール領域を確保する。
コード
下部に100の余分な領域を確保する
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 100, right: 0)参考にさせていただきました
UIScrollViewのコンテントオフセットとコンテントインセットについて
http://u16suzu.hatenablog.com/entry/2015/04/01/195309
- 投稿日:2021-01-09T10:22:22+09:00
【Swift】iOS14におけるIDFAの取得
IDFAとは
IDFA は「Identifier for Advertisers」の略称であり、Appleがユーザーの端末にランダムに割り当てるデバイスIDです。 広告主はこのIDを使ってユーザーの広告エンゲージメント 、アプリ内のユーザー行動を計測することで、カスタマイズした広告を配信することができます。
つまりiOSアプリ内で自分の興味のある広告表示されることがあるのは、このIDFAのおかげということになります。iOS14からのIDFAの取得
WWDC2020でiOS14でIDFAの扱いに変更が加えられることが発表されました。
iOS14以降はIDFAを取得する前に取得の可否を選択させるダイアログを表示することが義務付けられるようになりました。IDFA取得ダイアログの表示方法
1.frameworkの追加
以下のframeworkを追加します。
AdSupport.framework
AppTrackingTransparency.framework参考リンク:
https://developer.apple.com/documentation/adsupport
https://developer.apple.com/documentation/apptrackingtransparency2.info.plistへの文言の追加
info.plistに「Privacy - Tracking Usage Description」をKeyとしてValueに文言を設定します。
上記画像の赤枠の箇所に文言が反映され、他の箇所についてはデフォルト文言で変更不可のようです。3.ダイアログを表示
ダイアログ表示するにはrequestTrackingAuthorization(completionHandler:)を使用することで、下記のように実装することができます。
Swift
if #available(iOS 14, *) { ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in switch status { case .authorized: //IDFA取得 print("IDFA: \(ASIdentifierManager.shared().advertisingIdentifier)") print("success") case .denied, .restricted, .notDetermined: print("failure") @unknown default: fatalError() } }) }
- 投稿日:2021-01-09T06:04:21+09:00
【Swift】UIImagePickerより便利なUIPHPickerViewControllerを使ってみた
UIImagePickerControllerについて調べていると、PHPickerViewControllerというものに出会ったので使ってみた。
デモ
見た目の動きとしては、ほぼUIImagePickerControllerとほぼ同じです。
UIImagePickerControllerとの違い
・検索機能付き
・複数選択が可能
・プライバシーに配慮したプロセスになっており、Privacy Photolibrary Usageを設定する必要がない使ってみる
①PhotosUIをインポート
import PhotosUI②コンフィギュレイションを設定し、PHPickerViewController(configuration:)に値を入れる
var configuration = PHPickerConfiguration() //選択できる枚数を指定する。 0 を代入すると無制限に選択出来る configuration.selectionLimit = 1 //PHPickerViewに何を表示させるか選択出来る。 以下は画像のみ configuration.filter = .images //複数選択したい場合は、配列で選択が可能 //configuration.filter = .any(of: [.images, .livePhotos, .videos]) let phPicker = PHPickerViewController(configuration: configuration)③あとは、delegateを設定して表示するだけ
phPicker.delegate = self present(phPicker, animated: true)画像が選択された時のdelegate処理を書いてみる
extension ViewController: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { picker.dismiss(animated: true) //結果から最初のItemProviderを取得 let itemProvider = results.first?.itemProvider //オプショナルバインディングで取得したItemProviderのnilチェックとロード可能かチェック if let itemProvider = itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) { //ItemProviderから画像を呼び出す itemProvider.loadObject(ofClass: UIImage.self) { (readingImage, error) in if let error = error { print("画像の取得に失敗しました:", error) return } DispatchQueue.main.async { guard let image = readingImage as? UIImage else { return } self.imageView.image = image } } } }参考
UIImagePickerControllerに代わるPHPickerViewControllerの紹介
とても分かりやすく詳しく書かれており、とても参考になりました?♂️謎のエラー
シュミレーターでは問題なく動作したが、実機でPHPickerViewを使用したところ、
[ERROR] Could not create a bookmark: NSError: Cocoa 257 "The file couldn’t be opened because you don’t have permission to view it."
というエラーが出た。エラーをこちらで[PHPicker] Permission error調べたところ
この問題を認識しており、将来のリリースで修正される予定です。
とのことです。まぁ、動作はするからいいのか?
まとめ
奥が深いPHPImagePickerでした。今後の開発に活用していきたい。
何か間違いがありましたら、優しく教えていただけると幸いです。