20200918のiOSに関する記事は13件です。

Swift のOptionalをUnwrapする5つの方法 4個目はお勧め

1. Force Unwrapping

optional!

2. Check for ni value

if optional != nil {
    optional!
}

3. Optional Binding

if let safeOptional = optional{
    safeOptional
}

4. Nil Coalescing Operator ??

optional ?? defaultValue

5. Optional Chaining

optional?.property
optional?.method()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】AssetCatalogのColorSetを閲覧できるツールを作ったお話

Xcode9 iOS11からAssetCatalogで色も管理できるようになりました。大分前に導入された仕組みなので既に活用されているチームも多いかと思います。
単色の管理はもちろんですが、1つのColorNameに対してAny, Light, Darkの色を紐付けることができ、StoryBoard上ではColorNameを指定するだけでLight mode, Dark modeを自動で判別し色を切り替えてくれるのでダークモードに対応する際に効果を発揮してくれます。

しかし、同一のUIに対して複数の色が入り込んでくると「ColorNameがLight時は、このカラーコードで、Dark時はこのカラーコード...」などと混乱する場面がありました。
また、デザイナーさんはXcode側のAsset Catalogを直接見れるわけではないので現在リリースされているアプリ内で利用できるカラーコードリストを別で管理する手間などもあり課題を感じることが多かったです。

そんなことがあり、最新のプロジェクトに含まれるAssetCatalogを閲覧できるような仕組みがあれば、エンジニアもデザイナーも楽出来るんじゃないと思ったので...
とりあえず作ってみた

AssetCatalogの中身

例えばこんなColorSetがあったとします。
スクリーンショット 2020-09-10 10.48.58.png
実際は、下記のようなjsonで管理されておりXcode側がGUIで上手いこと表示しているに過ぎません。

contents.json
{
  "colors" : [
    {
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "1.000",
          "blue" : "0.000",
          "green" : "0.149",
          "red" : "1.000"
        }
      },
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "light"
        }
      ],
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "1.000",
          "blue" : "1.000",
          "green" : "0.198",
          "red" : "0.017"
        }
      },
      "idiom" : "universal"
    },
    {
      "appearances" : [
        {
          "appearance" : "luminosity",
          "value" : "dark"
        }
      ],
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "alpha" : "1.000",
          "blue" : "0.000",
          "green" : "0.977",
          "red" : "0.000"
        }
      },
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

上手いことContents.jsonをディレクトリから探し出して、Any・Light・Darkそれぞれのカラーコードを取得することが出来れば必要なデータは抽出することが出来ます。

AssetCatalogViewer

MasamiYamate/AssetCatalogViewer
Githubに上がっているリポジトリからAssetCatalogを探し、各ColorSetのContents.jsonからAny・Light・Darkのそれぞれのカラーコードを取得してViewer向けのデータに整形する簡単なお仕事をしてくれるツールを作りました。
jenkinsやBitriseなどCIと組み合わせて利用することを考えていたので、Githubに上がっているプロジェクトを対象にしています。

動作確認環境

nodejs v14.10.1

利用方法

1.npm installを行って依存モジュールをInstallします。

$ npm install

2.AssetCatalogを閲覧したいリポジトリのURLを設定します

$ npm run setup

> colorsetjsonparser@1.0.0 setup /Users/hogehoge/Documents/Workspace/AssetCatalogViewer
> bash ./deploy-script/setup.sh

Enter the clone url or ssh: 【github url or ssh】
audited 1312 packages in 4.365s

56 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Repository setting completed!!

3.masterブランチに適応されているColorSetを取得しViewを下記コマンドで生成します。

$ npm run build

Success!!

以上で完了です。
生成されたViewerページは、distディレクトリ配下にあります。
./dist/index.htmlを開くとこんな感じで表示されます。
スクリーンショット 2020-09-18 21.36.47.png

現段階では、ColorSetの名称とAny・Dark・Lightそれぞれのカラーコードを横並びで表示するだけです。
クリックしたらUIColorのコードが取得出来たりするともっといいのかなと思いますが、
とりあえずAssetCatalogをXcode上で開かずに簡単に見れるということは実現出来たかなと思っています。

文字色が黒固定なので、背景色によっては見辛いものがあったりしますが、今後対応してく予定です。

最後に

結構強引な手段ではありますが、目的を達成できそうなものは作れたかなと思います。
チームへの導入はまだ出来ていないので、エンジニア・デザイナーそれぞれからフィードバックをもらいつつさらにブラッシュアップをかけていきたいと考えています。
(とりあえず実現からだったので、実装は荒いです。改修に併せて、リファクタも進めます。)

このようなアプローチでエンジニア・デザイナー間のやりとりを楽にしようと思っているという1つの考えとして参考になれば幸いです。

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

クロージャと、@escapingと、循環参照。

image.png

 クロージャ

 使い方

変数引数に、関数の処理を直接代入する。

 使う理由

最近のプログラミング言語では、
引数に関数を入れるとか、戻り値に関数を入れるとかが当たり前になっているので、
そういうときにクロージャを使うと綺麗に記述できるので、みんな嬉しい。

  • 引数として使うパターンが多い

 名前の由来

Closure closes over var.

 コード

 let, varに、関数の処理を直接代入

// MARK: - let, varに、関数の処理を直接代入

// 定数closureが、恰も関数のように扱える
let closure = { () -> () in print("Hello World!")}
closure()

let closure_2 = { () -> Void in print("hamburger")}
closure_2()
// 戻り値がVoid型、引数がない場合は、(引数) -> 戻り値の型 in を省略可
let closure_3 = { print("Yeah!") }
closure_3()

// 型を指定
let closure_4: (Int, Int) -> Void = { (num1: Int, num2: Int) -> Void in print(num1 + num2) }
// 型推論ってやつ
let closure_5 = { (num1: Int, num2: Int) -> Void in print(num1 + num2) }
closure_4(100, 199)
closure_5(100, 199)

// 因みに、closure_4 は num1, num2を省略可。

// 内部引数名を省略 -> 「$」を使用。
// $0 は最初の引数を表し、$1 は2番目の引数を表します。
let closure_6: (Int, Int, Int, Int) -> Void = {
    print ($0 - $1 + $2 + $3)
}
closure_6(30, 20, 100, 10) // 120

 型推論とは?

型の指定をしなくても、代入した値に応じて値の型を推論してくれる機能。
つまり、Swiftは変数の宣言時に型の指定を省略できる。( -> むしろ推奨。)

 内部引数とは?

 関数を呼び出すときは、外部引数名を利用します。

 普段 私たちは、内部引数名外部引数名として、関数を呼び出しています。
 違いはこちら


 クロージャを引数として、関数を実行

  • 引数として使うパターンが多い
// MARK: - クロージャを引数として、関数を実行

func closureTest(num1: Int, num2: Int, closure: (Int, Int) -> Int) { print(closure(num1, num2)) }

// return文が1行のみの場合には「return」は省略可
closureTest(num1: 300, num2: 5000, closure: { (num1, num2) -> Int in return num1 + num2 })
closureTest(num1: 300, num2: 5000, closure: { (num1, num2) -> Int in num1 + num2 })

// 引数の型を入力しないと、こうなるので注意。
// closureTest(<#T##<<error type>>#>, <#T##<<error type>>#>, <#T##<<error type>>#>)

// トレーリング クロージャ (= Trailing Closure)
closureTest(num1: 100, num2: 400) { (num1, num2) -> Int in
    num1 + num2 // return省略
}

 Trailing Closureとは?

関数の引数のうち 最後の引数がクロージャの場合、
クロージャを( )の外に書くことができる。

func testprint(str1: String, closure: (String) -> Void) {
    closure("僕の名前は\(str1)です。")
}

// トレーリングクロージャの場合 -> 美しい
testprint(str1: "玄邪 太郎") { string in
    print(string)   
}

// 通常のクロージャの場合 -> 可読性が悪い...
testprint(str1: "玄邪 太郎",{ string in
    print(string)   
})
  • クロージャが引数だと、{ }の外に( )を包まないといけないので、 可読性が悪くなる。(-> そこで、Trailing Closureが考案)

 クロージャの、基本的な性質2つ

  • ?クロージャーは関数の引数変数として使える
  • ?クロージャーは自分が定義されたスコープをキャプチャする

【Swift】クロージャの理解

 引数としてのクロージャ、利点?

  • コードが綺麗になる。
  • 呼び出し側で、処理が記述できる

呼び出され側は、クロージャに引数として値を渡し、
「値は渡すから、その値はそちらで好きに料理してね」という具合です。

class Foo {
    let val:Int = 10

    func testClosure(closure: (Int) -> Void) {
        closure(self.val) // self
    }
}

class Bar {
    let foo = Foo()

    // 引数の値を倍にする
    func twice() {
        foo.testClosure{ arg in print(arg * 2) } // arg = argument = 引数
    }

    // 引数の値を半分にする
    func half() {
        foo.testClosure{ arg in print(arg / 2) }
    }
}

let bar = Bar()
bar.twice()    // 20
bar.half() // 5

 クロージャによる、 変数と定数のキャプチャ?

  • ローカルスコープ(= scope_1)で定義された変数や定数は、
    ローカルスコープ内でしか使用できませんが、(= scope_2では使用不可)

  • クロージャが参照している変数や定数は、
    クロージャが実行されるスコープ(= scope_4)が
    変数や定数が定義されたローカルスコープ以外(= scope_3以外)であっても、
    クロージャの実行時に使用できます。

これは、クロージャが 自身の定義されたスコープ(= scope_3) の
 変数や定数への参照を保持している為で、この機能をキャプチャと言います。

class Foo {
    // scope_2関数は、普通の関数scope_1を2回実行しています。
    func scope_1() {
        var toto = 1
        toto += 1 // toto = toto + 1 と同義 (= 変数totoの値を更新)
        print(toto)
    }

    func scope_2() {
        scope_1()
        scope_1()
    }

    // scope_4関数は、scope_3関数から返されたクロージャを2回実行しています。
    func scope_3() -> () -> Void {
        var tete = 10
        let closure = { tete += 1
            print(tete)
        }
        return closure
    }

    func scope_4() {
        let tutu = self.scope_3()
        tutu()
        tutu()
    }
}

let test = Foo()
test.scope_2() // 2, 2
test.scope_4() // 11, 12

// scope_2関数は、普通の関数scope_1を2回実行しています。

普通の関数は、実行後はリセットされるので、
1回目の実行で「2」と表示されても、実行後ローカル変数totoの値は「1」に戻ります。

なので2回目の実行でも「2」と表示されます。

// scope_4関数は、scope_3関数から返されたクロージャを2回実行しています。

クロージャが、scope_3関数のローカル変数teteを参照しているので、
1回目の実行で「11」、2回目の実行では1回目の参照を保持しているので「12」となります。


 キャプチャとは?

 自身の定義されたスコープの変数や定数への、参照を「保持」する機能。
 クロージャ特有の機能。

 クロージャにて、self を使う理由

 明示的なselfには、循環参照 が存在しないよう確認を促す役割がある。

(Escaping Closuresは)循環参照を起こす危険性があるので、
(プログラマにそれを意識させるために)クロージャーの中で親スコープを強参照するときは、selfをつける。

selfを付けることが、循環参照を防ぐことに直結しているわけではなくて
コードの意図が明確になり、循環参照しないかにプログラマが意識を向けて書くようになる。

 @escaping

image.png

 @escapingとは?

  • Swiftの 属性(= attribute)の1つ
  • @escaping属性は、クロージャに対して指定する追加情報
  • 関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す

 この記事分かりやすいです?

var array = [()->Void]()

func testEscaping(arg: @escaping ()->Void) {
    //関数のスコープ外の配列に追加
    //クロージャが関数外で保持されることになるので、@escaping属性が必要
    array.append(arg)
}
testEscaping {print("玄邪 一郎")}
testEscaping {print("玄邪 二郎")}

array.forEach { $0() } //Swiftではクロージャの引数に自動的に$0、$1、$2…と順に名前が付与されます。

 属性(= attribute)とは?

  • コンパイラに対し、宣言や型の補足情報を伝えるもの
  • @を使う。

 @escapingを、 使う理由

 複数のタスクを、非同期で並列処理させるため。

 どんな時に、 必要か

  • クロージャが、スコープ外で強参照されるとき (= プロパティとして保持されるとき)
  • クロージャを、非同期で実行するとき (= メソッド内ですぐに実行されないとき)

 こちら参考

クロージャをすぐに実行し、どこからも強参照されない場合は、
@escaping は必要ありません。

 よくある例

 非同期処理をする、完了ハンドラとしてのクロージャ。

// NG: コンパイルエラー
func someAsyncMethod(completion: () -> Void) {
    DispatchQueue.main.async {
       completion()
    }
}

// OK
func someAsyncMethod(completion: @escaping () -> Void) {
    DispatchQueue.main.async {
       completion()
    }
}
// completionHandler -> クエリが完了したときに実行される
// DispatchQueue -> 処理待ちタスクを追加するためのキュー

 DispatchQueueについて

 「循環参照」 に 気を付けよう?

 @escapingなクロージャは、どこか から強参照される可能性があります。
 その参照元をクロージャ内で強参照すると、循環参照になります...

 循環参照

image.png

 循環参照とは?

 お互いにインスタンスを参照しあうため、
 どちらも解放されずにそのまま残り続けてしまう現象。? (ぐるぐる...)

image.png

 なぜNG?

 生成したインスタンスが メモリから解放されないと、メモリ リークとなるから。
 「解放」 = "ここ使い終わったから どうぞ"?

 メモリ リーク (= Memory leak)

永久にメモリを消費し続ける現象。
再現性が低く、テストもデバッグも極めて困難な悪質なバグとして有名。

class Sample {}

// Sample クラスのインスタンスが生成されるが参照カウンタが 0 のままなのですぐに解放される
Sample()

// Sample クラスのインスタンスが生成されて sample に代入されているので参照カウンタが 1 となり解放されない
var sample: Sample? = Sample()

// nil を代入すると参照がなくなるのでインスタンスの参照カウンタが 0 になり解放される
sample = nil

 参照カウンタ

 TODO: 後述

 循環参照 を 防ぐには?

 弱参照をする。
 weak または unowned という修飾子を使う。(unknownedではない...!)

un owned: 所有されていない
un known: 知られていない, 不明な
un known ed -> こんな言葉ない

 weakunownedの違い

 TODO: 後述

 なぜweakが、 循環参照を防げるのか?

 TODO: 後述

 [ ] とは何か

 TODO: 後述

普段なにげなく書いている[unowned self]の意味を調べる
参照カウントについて
これも

おしまい

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

iOS14で「バックグラウンド再生」が出来ない不具合への対処

概要

現在、リリースしているメディア管理系アプリで行ったiOS14に関する不具合調査とその対策です。

経緯とバグ調査

アプリを使って頂いているユーザーさんから「iOS14でバックグラウンド再生が出来ない」

との問い合わせがありました。

早速、手元の実機で調査してみると

動画ファイルを再生中 → アプリをバックグラウンドへ移行 → 再生が一時停止されてしまう

という現象を確認出来ました。

これが、iOS14の仕様(であればアプリ側のバグ)なのか

iOS14側の不具合なのかは今のところ分かりません。

ただ、音声ファイルの再生時にこの現象は起きないので
(バックグラウンドでも再生は継続される)

おそらく、iOS14の新しい仕様の可能性が高いのかなと思います。

ちなみに一時停止してしまった動画は、

コントロールセンターからプレイバックボタンを押す事で、再び再生が可能でした。

対策

バックグラウンドで動画再生させるケースはあまりない?と思われがちだが

実際、弊アプリのユーザーさんでは音楽動画などをBG再生して聴かれるケースも多いです。

症状は動画ファイル再生時のみですが、取り急ぎでも対応が必要です。

まずAVAudioSessionで色々やってみる

AVAudioSession

AVFoundation FrameworkAVAudioSessionのカテゴリーを調整してみます。

AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback, options: [])

ここのmodeoptionsを調整してみます。

extension AVAudioSession.Mode {
    public static let `default`: AVAudioSession.Mode
    public static let voiceChat: AVAudioSession.Mode
    public static let gameChat: AVAudioSession.Mode
    public static let videoRecording: AVAudioSession.Mode
    public static let measurement: AVAudioSession.Mode
    public static let moviePlayback: AVAudioSession.Mode
    public static let videoChat: AVAudioSession.Mode
    public static let spokenAudio: AVAudioSession.Mode
    public static let voicePrompt: AVAudioSession.Mode
}
    public struct CategoryOptions : OptionSet {
        public init(rawValue: UInt)
        public static var mixWithOthers: AVAudioSession.CategoryOptions { get }
        public static var duckOthers: AVAudioSession.CategoryOptions { get }
        public static var allowBluetooth: AVAudioSession.CategoryOptions { get }
        public static var defaultToSpeaker: AVAudioSession.CategoryOptions { get }
        public static var interruptSpokenAudioAndMixWithOthers: AVAudioSession.CategoryOptions { get }
        public static var allowBluetoothA2DP: AVAudioSession.CategoryOptions { get }
        public static var allowAirPlay: AVAudioSession.CategoryOptions { get }
    }

色々と組み合わせてみるが、効果無し。

仕方がないので、少し強引な対策で対処

根本的な対策方法については、引き続き調べていくとして

とりあえずの落としどころが欲しかったので、

  • まずAVPlayerItemのstatusを監視

  • システムから再生が停止されてしまった際 → playbackLikelyToKeepUpの変更を検知
    isPlaybackLikelyToKeepUp

  • そのタイミングでバックグラウンド再生中であれば、再び動画を再生させる。

と言う方法で対処することにしました。

override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?){

    if let item = object as? AVPlayerItem, let keyPath = keyPath {
        if item == self.playerItem {
            switch keyPath {
                        // ...略
            case #keyPath(AVPlayerItem.playbackLikelyToKeepUp):
                debugPrint("PlaybackLikelyToKeepUp is changed")             
                // ** BG状態かつ、再生ステータスなのにAVPlayerが停止という状態をチェック
                if isInBackground, isInconsistentPlaybackState {
                                    play()
                  debugPrint("Background Playback Forced Resuming >> Did Execute")
                }
            default:
                break
            }
        }
     }

}

調査は継続しつつ、これで様子を見ていきたいと思います。

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

【Swift5】 iOSの音声合成(AVSpeechSynthesizer)の声を男性に変更する

概要

iOSでは標準で利用可能な音声合成(AVSpeechSynthesizer)が用意されている。
普通に利用する場合、女性の声が利用可能だが、声を男性に変更する方法をメモする。

AVSpeechSynthesizerの基本的な使い方については以下の記事を参照。
https://qiita.com/maKunugi/items/dc9da201a663c8773c8c

音声の変更

let voices = AVSpeechSynthesisVoice.speechVoices()

利用可能な声は、AVSpeechSynthesisVoiceクラスのspeechVoicesメソッドから取得が可能。

voiceの中身は下記のような内容。

name: Kyoko
gender(rawvalue): 0
lang: ja-JP
identifier: com.apple.ttsbundle.Kyoko-compact

speechVoicesメソッドから利用したい声を取得したら、AVSpeechUtteranceクラスにsetしてあげればOK。

let synthesizer = AVSpeechSynthesizer()
...
let utterance = AVSpeechUtterance(string:text)
utterance.pitchMultiplier = 1.0
utterance.voice = voice        
synthesizer.speak(utterance)

利用可能な日本語音声

手元の端末で確認できた日本語の音声はこちら。

- Otoya (Enhanced) #男性
ja-JP
com.apple.ttsbundle.Otoya-premium

- Hattori  #男性
ja-JP
com.apple.ttsbundle.siri_male_ja-JP_compact

- Kyoko #女性 (Default)
ja-JP
com.apple.ttsbundle.Kyoko-compact

- O-ren #女性
ja-JP
com.apple.ttsbundle.siri_female_ja-JP_compact


- Otoya #男性
ja-JP
com.apple.ttsbundle.Otoya-compact

上記の音声が確認できました。男性の声も2種類指定が可能なので、「Hattori」もしくは「Otoya」の音声を指定してあげれば男性の声で読み上げをすることが可能です。

注意事項

音声が端末で利用できるかの確認

何もしない場合はDefaultの女性の声(Kyoko)が読み上げに利用されます。
端末内にない存在しない音声を利用しようとするとクラッシュします。
そのため、端末で利用したい音声が存在しているかは必ず確認する必要があります。

var voice:AVSpeechSynthesisVoice? = voices.first{
    $0.identifier == "com.apple.ttsbundle.siri_male_ja-JP_compact"
}
if let voice = voice {
    ~~~~~
} else {
    ~~~~~
}

ユーザが音声をダウンロードすることで利用可能な音声が増える

初期状態ではほとんどの端末がDefaultの音声しか利用ができない。(シミュレータは増やせない模様)
Siriの設定やアクセシビリティの設定等で音声の変更を行うと、追加の音声データを端末にダウンロードができる。その手順を踏んだ端末には上述したような複数の音声が利用可能になる。
そのため、必ず男性の声でなければならないといった制約のあるアプリを提供したい際は、男性の音声データが端末にダウンロードされている状態にするため、うまくユーザを誘導する必要がある。

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

react-native-firebase(v6↑) + react-native-push-notificationを使ったプッシュ通知実装の注意点

まえがき

私が開発しているReact Nativeアプリについて、元々react-native-firebase(v5)を使ってプッシュ通知を実装していましたが、そろそろバージョンを上げることにしました。

ところが、v6からローカル通知の機能がNotifeeという有料の別ライブラリに分離されており、ローカル通知の機能のみ他のライブラリを採用せざるを得なくなりました。

そこで使えそうなライブラリを調べてみたところ、

  • Notifee: ローカル通知専用ライブラリ。react-native-firebaseのチームが作っており当然相性も良く、品質も高そうだが、有料
  • react-native-push-notification: リモート&ローカル通知ライブラリ。一番スターが多い。
  • react-native-notifications: リモート&ローカル通知ライブラリ。Wix製。Androidでchannelを指定してローカル通知を送れなさそう。(今のところ)

という感じのようだったので、react-native-push-notificationを採用することにしました。

本記事では、react-native-firebase(v6↑)react-native-push-notificationを使ってプッシュ通知を実装するにあたっての注意点を紹介します。

実現したいこと

  • FCMで受信したメッセージを、アプリの状態(Foreground/Background/Quit)にかかわらずプッシュ通知で表示したい
  • プッシュ通知をタップしたとき、アプリの状態にかかわらず処理したい

本記事で触れないこと

  • react-native-firebaseの導入方法
  • FCMの導入方法

環境

  • React Native (0.63.2)
  • @react-native-firebase/app (8.4.2)
  • @react-native-firebase/messaging (7.8.6)
  • react-native-push-notification (5.1.1)
  • @react-native-community/push-notification-ios (1.5.0)

ライブラリを導入する

※ react-native-firebaseについては導入が済んでいるものとします。

まずは、react-native-push-notificationを導入します。このライブラリは、iOS部分については@react-native-community/push-notification-iosを使用しているため、併せてインストールを行います。

npm install --save react-native-push-notification @react-native-community/push-notification
npx pod-install

続けて、iOSについて下記コードの追記を行います。

AppDelegate.h
#import <UserNotifications/UNUserNotificationCenter.h>
...
@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate, UNUserNotificationCenterDelegate>
AppDelegate.m
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>

...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  ...
  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
  center.delegate = self;

  return YES;
}

...

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
  [RNCPushNotificationIOS didReceiveNotificationResponse:response];
  completionHandler();
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}

push-notification-iosのREADMEの導入手順には、他にもリモート通知用のデリゲートメソッドがいくつか記載してありますが、今回はローカル通知用として使用するため、上記のデリゲートメソッドがあればOKです。

実装前に抑えておくポイント

アプリの状態

アプリには以下の3つの状態があり、状態によってプッシュ通知受信時およびタップ時の挙動が異なります。

  • Foreground: アプリが前面にあり動作している状態
  • Background: ホーム画面や他のアプリが前面にあり、アプリが背面で動作している状態
  • Quit: タスクキルなどでアプリが死んでいる状態

Quit時にプッシュ通知をタップした場合は、アプリが起動します。その起動したタイミングでタップしたプッシュ通知を処理することになります。

リモート通知とローカル通知

アプリがBackgroundまたはQuit状態のときにFCMを受信すると、FCM側(react-native-firebase)がプッシュ通知を表示してくれます。しかし、Foreground状態の場合には表示してくれず、代わりにonMessageコールバックが呼ばれます。そのため、onMessageの中で、react-native-push-notificationを使ってプッシュ通知を表示する処理を実施する必要があります。本記事では、前者のことをリモート通知、後者のことをローカル通知と呼んでいます。

実装する

関係するコードの抜粋です。

app.js
import PushNotification from 'react-native-push-notification';
import PushNotificationIOS from '@react-native-community/push-notification-ios';

class App extends Component {
  constructor(props) {
    ...

    PushNotification.configure({
      onNotification: notification => {
        // プッシュ通知をタップしたときに呼ばれる
        ...  // URLを開くなどの処理
        notification.finish(PushNotificationIOS.FetchResult.NoData);
      } 
    });
  }

  componentDidMount() {
    this.unsubscribeMethods = [
      firebaseMessaging().onTokenRefresh((token: string) => {
        // トークンリフレッシュ時に呼ばれる
        ...  // トークンリフレッシュ時の処理
      }),
      firebaseMessaging().onMessage(message => {
        // Foreground時にリモートプッシュ通知を受信した際に呼ばれる
        this._localNotification(message);        
      })
    ];
  }

  componentDidUnmount() {
    this.unsubscribeMethods();
  }

  // ローカル通知
  _localNotification(message) {
    PushNotification.localNotification({
      title: message.notification.title,
      message: message.notification.body,
      userInfo: message.data,
      ...
    });
  }
}



ForegroundでFCMを受信したときにはonMessageが呼ばれるので、localNotification()でローカル通知を行っています。

プッシュ通知がタップされたときはreact-native-push-notificationのonNotificationが呼ばれます。ローカル通知だけでなく、react-native-firebaseが表示したリモート通知がタップされたときも呼ばれます。また、Quit時にタップされてアプリが起動する場合も、起動時に呼ばれます。すなわち、プッシュ通知がタップされた際の処理は、あらゆる場合にonNotificationでカバーできることになります。

なお、@react-native-firebase/messagingには、リモート通知をタップした際に呼ばれるonNotificationOpenedAppや、起動時に呼ぶことでQuit時にタップした通知を取得するgetInitialNotification()が用意されていますが、リモート通知に限って処理を行いたいケースを除いては、特に使う必要はなさそうです。

タップしたときの動き(メモ)

各OS、各アプリ状態、ローカル通知/リモート通知について、プッシュ通知をタップしたときにどのコールバックが呼ばれるかを整理した際のメモを載せておきます。onNotificationでは、Foregroundでタップされたかどうかの値foreground、ユーザーがタップしたかどうかの値userInteractionが引数で渡されますが、一部のケースで誤った値になっていそうです(該当箇所は太字で記載しています)。

iOS

ローカル通知

  • Foreground: onNotification(foreground=true, userInteraction=false)が呼ばれる
  • Background: onNotification(foreground=false, userInteraction=true)が呼ばれる。
  • Quit: onNotification(foreground=false, userInteration=true)が呼ばれる。(getInitialNotification()では取れない。)

リモート通知

  • Foreground: onNotificationOpenedAppが呼ばれる。onNotification(foreground=true, userInteraction=false)も呼ばれる。
  • Background: onNotificationOpenedAppが呼ばれる。onNotification(foreground=false, userInteraction=true)も呼ばれる。
  • Quit: getInitialNotification()で取れる。onNotification(foreground=false, userInteraction=true)も呼ばれる。

Android

ローカル通知

  • Foreground: onNotification(foreground=true, userInteraction=true)が呼ばれる。
  • Background: onNotification(foreground=true, userInteraction=true)が呼ばれる。
  • Quit: onNotification(foreground=false, userInteration=true)が呼ばれる。(getInitialNotification()では取れない。)

リモート通知

  • Foreground: onNotification(foreground=false, userInteraction=true)が呼ばれる。onNotificationOpenedAppも呼ばれる。
  • Background: onNotification(foreground=false, userInteraction=true)が呼ばれる。onNotificationOpenedAppも呼ばれる。
  • Quit: getInitialNotification()で取れる。onNotification(foreground=false, userInteraction=true)も呼ばれる。

まとめ

  • ForegroundでFCMを受信した際にはプッシュ通知が表示されずonMessageが呼ばれるので、そのタイミングでreact-native-push-notificationのlocalNotification()を使ってローカル通知を表示する。
  • OS、アプリの状態、リモート通知/ローカル通知のにかかわらず、通知タップ時の処理はonNotificationで行えばOK。
  • onNotificationのforegroundやuserInteractionの値は怪しいので、場合分けには使わない方が無難。

以上、react-native-push-notificationの onNotification が万能で助かったというお話でした。

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

xcode12でsimulatorにビルドする時失敗する問題

問題

xcode12でsimulatorにビルドする時
building for iOS Simulator, but linking in object file built for iOS, for architecture arm64
のようなエラーが出てきます。

原因

https://developer.apple.com/documentation/xcode-release-notes/xcode-12-release-notes

The Build Settings editor no longer includes the Valid Architectures build setting (VALID_ARCHS), and its use is discouraged. Instead, there is a new Excluded Architectures build setting (EXCLUDED_ARCHS). If a project includes VALID_ARCHS, the setting is displayed in the User-Defined section of the Build Settings editor. (15145028)

Valid Architecturesはxcode12から削除されたのでエラーの原因になっていると思われる。

解決案

project.pbxproj内のVALID_ARCHSを削除することでビルドができるようになると思う。

image.png

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

Cordova iOSアプリでvideoタグの動画をインライン再生する

↓こちらの方法がiOS10以降から使えなくなっていたので
Cordovaを使ったiOSアプリでvideoタグをインライン再生

config.xml
<preference name="AllowInlineMediaPlayback" value="true"/>

playsinline属性を追加

<video src="sample.mp4" playsinline></video>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode 12 環境でアプリがビルドできない

Xcode 12

"Xcode 12 beta 2〜3"辺りから、
Carthage/CocoaPodsを使用している
自作アプリがビルドできなくなりました。
"Xcode 12"の正式版がでても解消されていません。

その原因、対策のまとめです。
各対策は、効果はありましたが、
暫定的なものか、恒久的なものかは分かりません。

Xcode 12 + Carthage

Carthageビルドの対策

carthage-build.sh
リンク先のcarthage-build.shをプロジェクトルートに作成し、
sh carthage-build.sh --platform iOS --no-use-binariesで実行する。

Carthageバージョンエラー

Carthage実行時に、"Carthage 0.35.1"にバージョンアップエラーが出た。
brew upgrade carthageで、"0.35.1"にバージョンアップしても、
繰り返しバージョンアップエラーが表示される。

[対策]
1. brew uninstall carthageでCarthageをアンインストール
2. https://github.com/Carthage/Carthage/releases/tag/0.35.1
 からCarthage.pkgをダウンロード>インストール

aws-sdk-ios + Carthage

"Xcode 12 + Carthage"環境で"aws-sdk-ios"がビルドできない。
>CocoaPodsではビルドできるため、CocoaPodsに移設する。

Xcode 12 + CocoaPods

Xcodeでビルド時に、
Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64
のビルドエラーになる。

CocoaPodsビルドの対策

Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

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

ScrollView のスクロール中だけ Lottie アニメーションを止めたい! (Swift)

はじめに

Lottie アニメーションを UIScrollView がスクロールしている最中のみ動かしたいときがあったので、その実装を紹介します。
具体的に言うと、

  • スクロール中: アニメーション停止
  • スクロール終了: アニメーション再開

という要求です。

準備: Lottie を動かす ViewController

ScrollView の動作関係なしに、ただ Lottie を動かす ViewController のコードを紹介します。
今回はこのコードを元に実装してみます。

コメント多めに書いてますので、ある程度は追えるかと思います。
今回はなんとなくランダムな位置に複数の Lottie アニメーションを配置してみました。

import UIKit
import Lottie

class ViewController: UIViewController {

    // MARK: - IBOutlet
    // scrollView の子要素で、 Lottie オブジェクトが配置される UIView
    @IBOutlet weak var contentView: UIView!

    // MARK: - Private properties
    var animations = [AnimationView]()

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        // ランダムな位置に 3 個の Lottie オブジェクトを生成・配置・再生する
        // 複数配置していることに特に意味はありません。
        for _ in 1...3 {
            let x = CGFloat.random(in: 1.0...5.0) * 200.0
            let y = CGFloat.random(in: 1.0...6.0) * 100.0
            let animation = generateAnimation(x: x, y: y)
            animations.append(animation)
            contentView.addSubview(animation)
            animation.play()
        }
    }

    // MARK: - Private methods
    // 引数で受け取った座標を持つ Lottie オブジェクトを生成するメソッド
    // see: https://qiita.com/ngo275/items/c9e94bad7a7afc85e4f4
    private func generateAnimation(x: CGFloat, y: CGFloat) -> AnimationView {
        let animationView = AnimationView(name: "sample_animation")
        animationView.frame = CGRect(x: x, y: y, width: view.bounds.width * 0.2, height: view.bounds.height * 0.2)
        animationView.loopMode = .loop
        animationView.contentMode = .scaleAspectFit
        animationView.animationSpeed = 1

        return animationView
    }
}

スクロール中だけアニメーションを止めてみよう

このままでは viewDidLoad ライフサイクルで play() されっぱなしで、止めようがありません。
今回の要求として、「スクロール中はアニメーションを止める」というのがありますので、まずは UIScrollView を Outlet 接続しましょう。

    // MARK: - IBOutlet
    // scrollView の子要素で、 Lottie オブジェクトが配置される UIView
    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var backgroundScrollView: UIScrollView! // 追加

次に、追加した backgroundScrollView の delegate 先を self つまり ViewController 自身に設定します。

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        backgroundScrollView.delegate = self // 追加

これで ScrollView の delegate メソッドが使えるようになりました。
UIScrollView の delegate メソッドは沢山ありますが、今回は

  • スクロールのし初め
  • スクロールの終わり

を検知することで

  • スクロールのし初め: アニメーションを一時停止
  • スクロールの終わり: アニメーションを再開

という風にして実装していきます。
沢山ある delegate メソッドの解説は 公式ドキュメント他の方がまとめてくださった記事 に譲って割愛します。
今回使うのは、以下の 2 つです。

    // 1.
    // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView)

    // 2.
    // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)

これらのメソッドを使えば、以下のようにして、簡単にスクロール中にアニメーションを止めて、スクロールが終わったらアニメーションを再開するということが可能になります。
ちなみに、 stop() メソッドを使用していますが、 pause() メソッドでも大丈夫です。
アニメーションが最後まで行って止まるか、 pause() された瞬間に止まるかの違いです。

    // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール開始と同時にアニメーションをストップ
        animations.forEach { $0.stop() }
    }

    // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール終了と同時にアニメーションをスタート
        animations.forEach { $0.play() }
    }

あとは、これを実装して終わりです。
ViewController に UIScrollViewDelegate を継承させます。
僕は extension して書いていますが、 class ViewController: UIViewController, UIScrollViewDelegate としても何も問題ありません。

// MARK: - UIScrollViewDelegate
extension ViewController: UIScrollViewDelegate {
    // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール開始と同時にアニメーションをストップ
        animations.forEach { $0.stop() }
    }

    // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール終了と同時にアニメーションをスタート
        animations.forEach { $0.play() }
    }
}

これでビルドしてみると、スクロールしている間だけアニメーションが止まることを確認できるはずです。
最後に、完成した ViewController を載せます。

import UIKit
import Lottie

class ViewController: UIViewController {

    // MARK: - IBOutlet
    // scrollView の子要素で、 Lottie オブジェクトが配置される UIView
    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var backgroundScrollView: UIScrollView!

    // MARK: - Private properties
    var animations = [AnimationView]()

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        backgroundScrollView.delegate = self

        // ランダムな位置に 3 個の Lottie オブジェクトを生成・配置・再生する
        // 複数配置していることに特に意味はありません。
        for _ in 1...3 {
            let x = CGFloat.random(in: 1.0...5.0) * 200.0
            let y = CGFloat.random(in: 1.0...6.0) * 100.0
            let animation = generateAnimation(x: x, y: y)
            animations.append(animation)
            contentView.addSubview(animation)
            animation.play()
        }
    }

    // MARK: - Private methods
    // 引数で受け取った座標を持つ Lottie オブジェクトを生成するメソッド
    // see: https://qiita.com/ngo275/items/c9e94bad7a7afc85e4f4
    private func generateAnimation(x: CGFloat, y: CGFloat) -> AnimationView {
        let animationView = AnimationView(name: "sample_animation")
        animationView.frame = CGRect(x: x, y: y, width: view.bounds.width * 0.2, height: view.bounds.height * 0.2)
        animationView.loopMode = .loop
        animationView.contentMode = .scaleAspectFit
        animationView.animationSpeed = 1

        return animationView
    }
}

// MARK: - UIScrollViewDelegate
extension ViewController: UIScrollViewDelegate {
    // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール開始と同時にアニメーションをストップ
        animations.forEach { $0.stop() }
    }

    // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print(#function) // どの関数が呼ばれているか確認用に表示
        // スクロール終了と同時にアニメーションをスタート
        animations.forEach { $0.play() }
    }
}

参考文献

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

Flutter プラグインの中で CoreData のモデル (.xcdatamodeld) を読み込む

実際のところ Flutter プラグイン固有の Tips ではなく、 Pods や Framework の中から .xcdatamodeld をロードする方法の Tips ですが、iOSネイティブ開発慣れしていないと、とても分かりにくいと思ったのでメモとして残しておきます。

問題

Core Data の使い方ガイドでは、以下のようなコードでサクッと MyModel.xcdatamodeld を読み込む方法が紹介されていますが、Flutter プラグインの中に .xcdatamodeld ファイルをもたせた場合、このコードを書いても読み込めません(ちなみに、この段階ではエラーにならず、エンティティ生成時にクラッシュします)。

Objective-C
_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"MyModel"];

原因

問題は2点です。

  1. Flutter プラグインは、Flutter アプリ本体とは別のフレームワークとしてビルドされますが、Flutter プラグインの初期設定では、.xcdatamodeldがバンドルされません。
  2. フレームワークにうまく MyModel.xcdatamodeld をバンドルした場合でも、モデル名だけではリソースを読み込めません。フレームワーク内のリソースには、そのリソースを指すパスでアクセスする必要があります。

対策

まず、.xcdatamodeld がフレームワークにバンドルされるようにします。
次のスクリーンショットは plugin_example という名前の Flutter プラグインのフォルダ構造です。
ios/Resources フォルダを作り、そこに MyModel.xcdatamodeld ファイルを置きます。

Screen Shot 2020-09-18 at 6.48.19.png

そして、podspec に次の行を追加します。

.podspec
  s.resources = 'Resources/MyModel.xcdatamodeld'

次に、モデル読み込みのコードを以下のとおり書き換えます。

Objective-C
NSURL *modelURL = [[NSBundle bundleForClass:self.class] URLForResource:@"MyModel" withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"MyModel" managedObjectModel:model];

self.class の self は Flutter プラグインに所属しているクラスのインスタンスを与えます。Flutterプラグインの初期化コードあたりに書けば良いと思います。

拡張子を momd に指定しているのは、MyModel.xcdatamodeld ファイルはビルドされ、 実際には MyModel.xcdatamodel.momd という名称でバンドルに保存されるためです。

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

Firebase Crashlyticsを導入してCould not get GOOGLE_APP_ID in Google Services file from build environmentが出た場合の対処法

エラー発生

iOSのプロジェクトにFirebase Crashlyticsを導入したところ、以下のエラーが発生。

Could not get GOOGLE_APP_ID in Google Services file from build environment

GoogleService-Info.plistは正しいパスに置いてあるはず。何故…

原因

公式ドキュメントに書いてありました。

注: 新しいビルドフェーズがプロジェクトの最後のビルドフェーズであることを確認してください。そうでないと、Crashlytics が正しく初期化されません。

対象のプロジェクトではXcodgenでプロジェクトファイルを生成していました。
Xcodegenは必ず最後にCocoaPodsのスクリプトが走るようになっているため、正しく初期化されなかったようです。
(postCompileScriptsよりも更に後に走る)

解決策

CocoaPodsのscript_phaseで、最後のビルドフェーズにスクリプトが走るようにして解決。

Podfile
target 'Project' do
    pod 'Firebase/Crashlytics'

    script_phase :name=> 'FirebaseCrashlytics',
                 :script=> '"${PODS_ROOT}/FirebaseCrashlytics/run"',
                 :input_files=> ['$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)']
end

ビルドフェーズの最後に追加されたことが確認できます。

Build Phases
スクリーンショット

参考文献

https://qiita.com/yosshi4486/items/73a627c1e3daf8f4f509
Fabricだった頃からあるようです。Xcodegenにスクリプトまとめたいですね…

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

Firebase Cloud Functions を TypeScriptで実装するための環境準備

A. 概要

 これからFirebase Cloud Functions を TypeScriptで実装しようとしている初心者向けに、その環境の準備手順を纏めます。

B. 目次

  1. Firebaseの準備
  2. TypeScriptを使用する準備

C. 詳細手順

1. Firebaseの準備

Reference: Firebase 公式ドキュメント
1) Firebaseプロジェクトを作成する ・・・ 委細省略
2) Node.js と Firebase CLI を設定する ・・・ クライアントPC毎にワンタイム
  a. Node.jsをインストール(公式サイト) *JavaScript実行環境
  b. npmをインストール *Node.jsのパッケージ管理システム
  c. Firebase CLI をインストール

// npmを使用してインストールする場合
npm install -g firebase-tools

  d. Firebase CLI と SDK の両方を更新

// Firebase プロジェクトの functions フォルダ内で次のコマンドを使用
npm install firebase-functions@latest firebase-admin@latest --save
npm install -g firebase-tools

3) Firebase SDK for Cloud Functions を初期化する
  a. firebase login を実行してブラウザからログインし、firebase ツールを認証
  b. Firebase プロジェクトのディレクトリに移動
  c. firebase init functions を実行。以下、実行例(既作成のプロジェクトがある場合)

(ターミナルの実行結果例)
hogeMBP:$ firebase login
(略)

hogeMBP:$ firebase init functions

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/fuga/Xcode/foo


=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Don't set up a default project  // このオプションを選択

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? TypeScript  // TypeScriptを選択
? Do you want to use TSLint to catch probable bugs and enforce style? Yes  // YESを選択
✔  Wrote functions/package.json
✔  Wrote functions/tslint.json
✔  Wrote functions/tsconfig.json
✔  Wrote functions/src/index.ts
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes  // YESを選択

> protobufjs@6.10.1 postinstall /Users/fuga/Xcode/foo/functions/node_modules/protobufjs
> node scripts/postinstall

npm notice created a lockfile as package-lock.json. You should commit this file.
added 285 packages from 217 contributors and audited 285 packages in 9.616s
found 0 vulnerabilities


i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!

なお、上記で選択した以外のオプションを選択すると、下記のようなエラーが発生した。。
**これは、Authエラーがあったため。本来なら、「Use an existing project」を選択すべきでした。上記のようにプロジェクトを追加しないで先に進めると、デプロイの前に、「firebase use --add」を実行する必要があります。
関連:Cloud Functions for Firebase で deploy 先を切り替える

(エラーの例1)
? Please select an option: Use an existing project  // 「Use an existing project」を選択
Error: Failed to list Firebase projects. See firebase-debug.log for more info.
(エラーの例2)
? Please select an option: Create a new project  // 「Create a new project」を選択
i  If you want to create a project in a Google Cloud organization or folder, please use "firebase projects:create" instead, and return to this command when you've created the project.
? Please specify a unique project id (warning: cannot be modified afterward) [6-30 characters]:
 hogehoge  // FirebaseのプロジェクトIDを入力
? What would you like to call your project? (defaults to your project ID) hogehoge  // FirebaseのプロジェクトIDを入力
✖ Creating Google Cloud Platform project

Error: Failed to create project. See firebase-debug.log for more info.

この処理で、クライアントPCのプロジェクトフォルダ内に、以下の構成が生成されます。
TypeScriptを利用する上で必要となる「package.json」や「tsconfig.json」はここで自動生成されます。

Firebase 公式ドキュメント

(ファイル構成)
myproject
 +- functions/     # Directory containing all your functions code
      |
      +- package.json  # npm package file describing your Cloud Functions code
      |
      +- tsconfig.json
      |
      +- tslint.json # Optional file
      |
      +- src/     # Directory containing TypeScript source
      |   |
      |   +- index.ts  # main source file for your Cloud Functions code
      |
      +- lib/
          |
          +- index.js  # Built/transpiled JavaScript code
          |
          +- index.js.map # Source map for debugging

2. TypeScriptを使用する準備

TypeScriptコンパイラをインストール

Reference: TypeScriptチュートリアル① -環境構築編-

a. (Node.jsをインストール) *上記で既にインストール済み
b. TypeScriptコンパイラをインストール

// npmを使用してインストールする場合
npm install -g typescript

c. インストールの確認 ・・・ ターミナルで「tsc -v」を実行し、バージョンが表示されたらOK

IDEを利用する準備

a. プロジェクトフォルダを開く。上記のフォルダ構成の場合、「functions」の位置(Not「src」)
b. ビルドタスクの設定(tasks.json)

Visual Studio Code のメニュー 「ターミナル」 
> 「既定のビルドタスクの構成」を選択
> 「tsc: ビルド - tsconfig.json」 を選択

または

shift + command + p
> 「Tasks: Configure Default Build Task」を開く
> 「tsc: ビルド - tsconfig.json」 を選択

作成した「tasks.json」は以下のような内容。

tasks.json
{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "typescript",
            "tsconfig": "tsconfig.json",
            "problemMatcher": [
                "$tsc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "label": "tsc: ビルド - tsconfig.json"
        }
    ]
}

c. 起動設定(launch.json)

「実行」 
> 「構成の追加」を選択
> 「Node.js」を選択

作成した「launch.json」は以下のような内容。
ここで、「preLaunchTask」の値は、「tasks.json」の"label"の値に変更。

launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "プログラムの起動",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}/lib/index.js",
            "preLaunchTask": "tsc: ビルド - tsconfig.json" 
        }
    ]
}

ここまでの設定で、VS Codeでデバッグ、コンパイルを実行できます(「実行」>「デバッグの開始」)。

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