20210109のSwiftに関する記事は9件です。

【Swift】超シンプルな課金の実装

難しいと感じる課金の実装をシンプルにまとめてみました。
コピペして実装して頂くととっても簡単にできます。必要に応じて適宜カスタムしつつ、活用してもらえたら嬉しいです。

実装全体

StoreManager.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となっています。
これはファミリー共有の機能によるものです。

関連記事

【iOS】iOS14でSand Boxアカウントの場所が変わった

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

[Xcode]README.mdファイルの追加方法

表題の通りXcodeにREADME.mdファイルを追加する方法について投稿します。

検索をかけたところ最近の記事にありつけなかったので、ぜひ2021年最新版として参考にしていただければと思います!

追加手順(シンプル)

Xcodeナビゲーションバー(Macディスプレイ最上部のメニューバー)ののFileを選択

Newを選択

Fileを選択

すると画像のようにファイルを選択するメニューが出てくるので、Markdown Fileを選択してNext
image.png

そのままページが切り替わるのでCreateを選択

これでGitにコミットすればGithubのリポジトリでREADMEに加えた編集内容を見ることができます。

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

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

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

はじめに

前回の記事を読んでない方に、簡単に説明するとiOSアプリで位置情報を取得するときに
配慮する点をまとめた内容となっています。

前回までのあらすじ

前回、端末の位置情報サービスの有無のチェックは
アプリをインストール後の初回起動のみしかされないと書きました。

初回起動時に位置情報サービスがオフになっていたらこんなアラートを表示します↓

このチェックは初回起動のみなので、2回目以降の起動時はアラートを表示するように改修しましたね↓

今回は、バックグラウンド状態からフォアグラウンド状態にアプリを切り替えた時にも
端末の位置情報サービスの有無をチェックするようにしていきましょう。

バックグラウンド状態から戻った時にチェックする(端末)

アプリが、バックグラウンド状態からフォアグラウンド状態に切り替わった時に
端末の位置情報サービスの有無をチェックするように処理するには

AppDelegateapplicationWillEnterForeground(_ 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: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます")
   }
}

ビルドしてみると、バックグラウンド状態から戻った時にもアラートが表示されるようになりました↓
ezgif.com-gif-maker.gif
これで、端末の位置情報サービスの有無を起動時バックグラウンド状態から戻った時
チェックするようになりましたね。

後、チェックしなければいけないのはアプリの位置情報サービスの有無です。

アプリの位置情報サービスの有無をチェックする

位置情報を取得する際には、ユーザーに許可を貰わなければ取得出来ません。
なので、アプリはこのようなアラートを表示してユーザーに許可をリクエストします↓

ですが、もし、ユーザーが許可しないを選択したらどうでしょうか?
位置情報が必要なアプリの場合、許可を貰わなければ困りますよね。

なのでユーザーが、許可しないを選択した場合にアラートなどを表示したりして
アプリの位置情報取得の許可を促さなければいけません。

前回のソースコードを例に説明します。
※ちょっと長いので一部抜粋

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

ViewController
import 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スキームで設定画面に遷移する方法

この方法を用いて、先ほどのソースコードに追加していきます。

ViewController
import 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
        }
    }
}

こちらをビルドしてみると、ちゃんと設定アプリに画面遷移しました。
ですが、ここでも落とし穴があります。
ezgif.com-gif-maker2.gif

この方法で、設定アプリに移動したのはいいけど
アプリの位置情報取得の許可をしないまま戻ったとしましょう。

このままだと、アプリは何もチェックしません。

この場面でも、アプリがバックグラウンド状態から戻った時に
アプリの位置情報取得の有無をチェックしなければいけません。

またまた記事が長くなりそうなので、今回はここら辺で終わります。
ここ間違っているよー!というのがありましたら、気軽にコメントして下さい。

最後まで読んで下さって、ありがとうございます!
ここまでのソースコードを下に載せておきます↓

ソースコード

ViewController
import 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))")
    }
}
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で続きを読む

modal遷移をフルスクリーンで、下から出ないように行う

全画面で遷移したいとき、下から出したくないとき、ありますよね。
以下はUIButtonaddTargetし、遷移する関数です。

@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
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ワールド

UnsafePointerワールド.png

UnsafeMutablePointerワールド

UnsafeMutablePointerワールド.png

immutableとmutableの違い

値自体には、特段の違いはありません。
また相互に初期化メソッドを通して変換も可能です。

ただmutable系はstatic関数として、allocateメソッドが用意されています。

UnsafePointerの生成.swift
func 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への変換は暗黙的変換によるキャストを使うと楽.swift
func 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イニシャライザでの暗黙的変換は動作未定義.swift
let 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する.swift
func 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する必要があります。

メモリーリーク.swift
struct 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を保持してしまっている

ダングリングポインタ.swift
struct 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ライブラリ側でダングリングポインタが発生する可能性.swift
func callDoSomething() {
    let num = 5
    doSomething(&num)
    // この関数が終わるとnumは廃棄される
}

func doSomething(_val: UnsafePointer<Int>) {
    // cライブラリ上の関数に渡す
    call_c_func(val)
}

上記、cライブラリ上でvalを持ち回してしまった場合、そのポインタ変数はダングリングポインタとなってしまっています。

範囲外へのアクセス

UnsafePointer、UnsafeRawPointerは、+演算子によって次のポインタにアクセスが可能ですが、
範囲を超えるとクラッシュします。

範囲外へのアクセスはクラッシュする.swift
func 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で値を取り出し、
渡す場合は、値の中身が開放されないように、責任を持ってallocatedeallocで管理しましょう。

つまり危険です。

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

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のライフサイクルのおさらい

https___qiita-image-store.s3.amazonaws.com_0_326574_354ea62b-ca5a-1136-4524-de841e7d7b89.png

ログイン後にホームの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.swift
import 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.swift
import 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.swift
import 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なはず
    }
}

参考

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

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

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

【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/apptrackingtransparency

2.info.plistへの文言の追加

b1b425f7-b494-2d34-86d9-d7bfd25bdb08.png

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()
      }
   })
}       
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】UIImagePickerより便利なUIPHPickerViewControllerを使ってみた

UIImagePickerControllerについて調べていると、PHPickerViewControllerというものに出会ったので使ってみた。

デモ

見た目の動きとしては、ほぼUIImagePickerControllerとほぼ同じです。
イメージ.GIF

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でした。今後の開発に活用していきたい。

何か間違いがありましたら、優しく教えていただけると幸いです。

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