【swift】AVFoundationを使って、動画の撮影を行う!

こんにちは。
先日以下のような記事を書かせていただきました。

kz-ike.hatenablog.com


前回は、AVFoundationに関する概念のお話でしたので、今回の記事では「動画を撮影する際に使用するクラス、及び必須設定項目」についてまとめようと思います。

※動画を追加する設定項目についてのみ説明するため、sessionクラスへinputを追加するや、session自体の開始といった方法は省略します。

最初に

動画を撮影して表示しるソースは以下に格納しました。

sampleAVFoundation/avFoundation at main · d-ike99/sampleAVFoundation · GitHub

AVFoundationに関するコードは、「ViewController.swift」に全て記載してます!

動画を撮影するのに必要なクラス

クラス名 概念
AVCaptureSession インプット、アウトプットに関する「セッション」を制御する
AVCaptureDeviceInput AVCaptureSessionの入力データとして定義するもの
AVCaptureVideoDataOutput インプットで取得した動画に対して、「記録」、「1フレーム毎へのアクセス」を可能とするアウトプットクラス

各クラスで必要な設定

AVCaptureSession

設定項目
変数 説明
sessionPreset AVCaptureSession.Preset アウトプットの解像度、ビットレートを定義する
補足

下記リンクで、Presetに定義できる変数がまとめられています。
それぞれの変数がどういったシーンで有効か全て把握できてませんが、上記のgithubでのサンプルプログラムでは「hd1920x1080」を指定しています。

https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture

AVCaptureDeviceInput

設定項目
変数 説明
device AVCaptureDevice インプットとしてどのデバイス(マイク、カメラなど)を使用するか定義する
補足

動画を撮影する際に指定するコードです。

guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .front) else {
    return
}

// インプットの定義
guard  let createInputInfo = try? AVCaptureDeviceInput(device: captureDevice) else {
    return
}

AVCaptureVideoDataOutput

設定項目
変数 説明
videoSettings [string: Any] 撮影した動画(画像)のフォーマットを設定する
alwaysDiscardsLateVideoFrame bool 「撮影したフレーム」のデータを、すぐに削除せずにメモリに残しておくかを指定する。単純に動画を表示するだけなら基本trueで問題ない
setSampleBufferDelegate - ・sample buffer(撮影した動画の一枚一枚のフレーム情報)の出力先の指定(delegateを定義する)
・上記delegateを呼び出すためのqueueの指定
補足

今回の記事では特に触れませんが、「マルチスレッド」の概念を抑えておくとコードを読みやすいです。
今回の動画を撮影するケースですと、以下のようなスレッドが登場すると思います。

①動画を撮影するスレッド
②撮影した動画の一枚一枚のフレームを出力するスレッド(setSampleBufferDelegateで定義するqueue)

videoOutput.setSampleBufferDelegate(self, queue: sessionQueue)

③「sample buffer」として取得した画像を、「画面に表示するため」のスレッド

DispatchQueue.main.sync {
    self.dispFrame(didCaptureFrame: image)
}


↓マルチスレッドを勉強した参考リンク
【Swift】Grand Central Dispatch (GCD)とOperationQueue まとめ - Qiita

さいごに

今回は、動画の撮影方法のみ言及しましたが、動画を保存するとなると別途手順が必要です。
私たちが何気なく使っている「カメラアプリ」もいろんな技術が詰まっていると改めて実感しました・・・!

【git】マージを理解する - ブランチのお供に!!

はじめに

本日は、マージの種類・方法についてまとめます。

先日、【git】"git-flow"を齧る - ドラムと筋肉とプログラミングという記事を書かせていてだきましたが、gitで複数のブランチを管理していくとなると「ブランチを切る・マージする」といった作業が必要になります。


マージの方法というのはいくつかありまして、マージに関して検索すると、「rebase」、「merge」、「fast-foward」といった用語が登場します。

それぞれどういった特徴なのか理解していないと、なんとなーく使用してしまいます。そしてコミットログが汚くなります。

コミットログが汚くなるのを避けるために、git-flowなどを用いてブランチを管理するためにも、マージの方法について理解していきます。

mergeとは?

概念

あるブランチの作業履歴(コミット)を、自分のブランチに取り込むこと

種類

  • Fast-fowardマージ
  • 3wayマージ(Non-Fast-fowardマージ)
  • rebase

いきなり「マージ」とは関係のなさそうな単語が登場しましたね・・・
しかし、一つ一つ見ていけば決して理解し難いものではないです。それぞれ説明していきます。

Fast-fowardマージ

概念

ブランチの「ポインタ」を"前に進める事"によるマージ

解説

概念だけ聞かされると何を言ってるかイメージしづらいと思います。
実際に作業するシーンも踏まえて説明します。


1. 新たなブランチの導入
 masterブランチで「バグ」が見つかったため、ブランチを切り(hotfix)修正した(C4コミットを切った)
https://git-scm.com/book/en/v2/images/basic-branching-4.png

2. masterブランチへの反映
 hotfixブランチでの修正作業が完了したため、masterブランチをhotfixブランチと同じ状況にしたい

// mergeコマンドの実行
$ git checkout master
$ git merge hotfix

Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

3. 結果
 masterブランチのポインタ、すなわち「コミットの位置」がhotfixと同じ場所に移動しました。
https://git-scm.com/book/en/v2/images/basic-branching-5.png

補足

上記のような例を出しましたが、言い換えると派生元のブランチが「更新されることがないことを見込める」の時に使用可能です。
上記の例で、hotfixで修正中にmasterブランチでも別途コミットを切ってしまった場合は、以下の3wayマージを使用する必要があります。

3wayマージ(Non-Fast-forwardマージ)

概念

3wayは3つのコミットを表しています。

①マージ元ブランチのポインタに該当するコミット
②マージ先ブランチのポインタに該当するコミット
③①、②の共通祖先となるコミット

「①と③の差分」と、「②と③の差分」をマージ元ブランチに取り込む方法を「3wayマージ」と言います。

解説

こちらも実際に作業するシーンも踏まえて説明します。


1. 新たなブランチの導入

  • masterブランチでバグが見つかったので修正用のブランチを切った(iss53)
  • iss53ブランチにて修正作業を実施した(C3, C5コミットを切った)
  • masterブランチで非バグの修正を行った(C4コミットを切った)。

https://git-scm.com/book/en/v2/images/basic-merging-1.png


2. masterブランチへの反映
 iss53ブランチでの修正作業が完了したので、masterブランチにマージをします。
 コマンドは「fast-fowardマージ」の時と同様ですが、ブランチを切ったコミット位置(C2)からマージ元ブランチ(このケースだとmaster)のコミットが進んでいます。

 そのため、masterブランチの差分(C2-C4の差分)とiss53ブランチの差分(C2-C5の差分)をそれぞれ抽出し、結合したものをmasterの新しいコミットとします(3.結果のC6)。

// mergeコマンドの実行
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

3. 結果
3wayマージをした結果になります。
iss53ブランチのコミット位置は変わりませんが、masterブランチはiss53ブランチのコミットを取り込んだ結果として、C6コミットが新しく生成されました。

このC6は3wayマージにより自動的に生成されるコミットでして、「マージコミット」といいます。

https://git-scm.com/book/en/v2/images/basic-merging-2.png

補足

コンフリクトについて

3wayマージなどで、それぞれのブランチの「共通の親」からの差分の箇所が同一となる場合、コンフリクトが発生します。

この場で詳細な修正手順は省きますが、
競合した該当ファイルはマージ元、マージ先の差分のどちらか(両方も可能)をファイルに取り込むよう修正して、コミットしてあげる必要があります。
先ほど、3wayマージでは「マージコミット」が自動的に生成されると話しましたが、コンフリクトした場合は手動で「マージコミット」を切る必要がある、ということがポイントです。

リベース

概念

基本は、マージと同じ考えです。

あるブランチの作業履歴(コミット)を、自分のブランチに取り込むこと

解説

こちらも実際に作業するシーンも踏まえて説明します。


1. 新たなブランチの導入

  • masterブランチで新たな機能を追加したいため、ブランチを切った(experiment)
  • iss53ブランチにて新規機能を追加した(C4コミットを切った)
  • masterブランチでバグが見つかったので修正した(C3コミットを切った)

https://git-scm.com/book/en/v2/images/basic-rebase-1.png


2. masterブランチの取り込み
 masterブランチでのバグ修正作業が終わったため、experimentブランチにマージします。

// rebaseコマンドの実行
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command


3. 結果
リベースした結果になります。

https://git-scm.com/book/en/v2/images/basic-rebase-3.png

experimentブランチのC4コミットは、C4'コミットという形でmasterブランチの先頭に切られました。

experimentブランチ上にmasterブランチが存在するような形になりましたので、「Fast-fowardマージ」により「masterブランチ」を「experimentブランチ」に移動することが可能となりました。

3wayマージと違いコミットが一直線となっているため見た目も綺麗です。

補足

リベースの多用はNG

一度リモートリポジトリにプッシュしたコミットに対してリベースは絶対に実施してはならないです!!

3.結果で少し触れましたが、リベースによって複数のコミットログをまとめた場合でも、それがプッシュ済みのコミットである場合、後で復活してしまう恐れがあります(要はコミットログがめちゃくちゃになって履歴の確認が困難になってしまう)。

そのほかにも、リベースを使用してはいけないと感じたケースがありますが、それはまた次の機会にご紹介したいと思います。

さいごに

git-flowなどのブランチ戦略を導入するとなると、「ブランチを切る」、「マージする」という作業がいやというほど登場してきます。

マージするときに、どのマージ手法を選択すれば良いか?その都度迷わないように、ブランチを運用する前に上記の内容をしっかり抑えておくと良いと思います。

【git】"git-flow"を齧る

はじめに

本日はタイトルの通り、"git-flow"についてご紹介します。

gitでは簡単にブランチを切れて便利ですが、何も考えずにとりあえずブランチを切って作業してしまうと後の管理が困難になってしまいます。

そこで、"新規機能開発"、"リリース"といったソフトウェアの開発作業における"作業工程(目的)毎"にブランチを切って、それぞれのブランチで作業を遂行する
というブランチ戦略の考えが登場しました

この"Git-flow"は、gitのブランチを管理するプライグインでして、ブランチ戦略、リリース管理をサポートするものとなっています。

筆者も「Myトレ」などでGitを使用していてこの"Git-flow"の考えはなんとなく取り入れていたのですが、なんとなくで留まってしまっていたため改めて理解を深めたいと思います。

本記事では、"git-flow"におけるブランチ戦略の考えと、実際の作業工程で具体的にどのような作業をするのか?をまとめます。

参考

「Vincent Driessen氏」が"A successful Git branching model"という記事にて紹介しています。

原文:A successful Git branching model » nvie.com
翻訳:見えないチカラ: A successful Git branching model を翻訳しました

"Git-flow"で用いるブランチの説明

ブランチの種類

  • メインブランチ

メインブランチは、"開発"と"リリース"における最新の状態を管理します。サポートブランチと違い、永久的に管理します。

ブランチ名称 分岐元、マージ先 役割
master 分岐元: -
マージ先: -
製品として出荷可能な状態を常に反映する
develop 分岐元: master
マージ先: master
「次のリリース」のための「最新の開発作業」の変更を反映する

★masterのソースはdevelopブランチなどからマージすることで成長させます。直接masterブランチにてソースコードを修正してはいけないです

  • サポートブランチ

サポートブランチは、作業のため一時的に作られるブランチです。作業としての役目を終えたらブランチ自体を削除します。

feature 分岐元: develop
マージ先: develop
developブランチから派生して、将来的にリリースの視野に入るような機能を開発する。
開発した結果、必要となったらdevelopにマージ、そうでなければ破棄をする
release 分岐元: develop
マージ先: master, develop
開発ブランチでリリースの準備ができたあとで、リリース完了するまでにソースの調整を行う
hotfix 分岐元: master
マージ先: master, develop
「develop作業中でリリース見込みがまだ立てられない、かつ緊急で修正すべき内容がある」といったケースで、本ブランチを生成して修正の作業を行う

登場したブランチを図にしたもの

https://nvie.com/img/git-model@2x.png

実際の作業プロセス

前提

  • githubの使用を想定しているため、github特有の操作を含んでいます。
  • 今回は、「開発工程」、「リリース工程」、この二つのプロセスについて説明します。
  • 具体的に説明するために、審査に提出する工程も含んでいます。

★俺流なところもあるかもですがご了承ください!

開発工程

①開発環境の作成 - 開発用ブランチの作成
 ブランチ:master → develop or develop → feature
 コマンド:

git branch -a // 現在のブランチ、及びリモートブランチ含めた全ブランチを確認する
git checkout -b develop // developブランチの作成(master → develop)
git checkout -b feature_hogehoge // featureブランチの作成(develop → feature)

②作業内容の決定 - "issue"の作成
 githubにてissueを作成する

④開発作業 - "issue"に対して改修の実施
 実施作業:commit, push
 コマンド:

git commit -m "#{issueの番号}" // issueに対して、コミットを紐付ける
git push origin feature_hogehoge  // feature_hogehogeブランチをgithubにプッシュ

⑤レビュー依頼 - pull requestの発行
⑥レビュー作業 - マージ or pull requeseの取り消し

リリース工程

①リリース準備 - リリースブランチの作成
 コマンド:

git branch -a // 現在のブランチ、及びリモートブランチ含めた全ブランチを確認する
git checkout -b release_v1.0.1 // releaseブランチの作成(develop → release)

②審査提出用ファイルの生成 - アーカイブファイルの生成
③(審査で不備がある場合、)修正作業
 修正後コミットを切る

★審査が通るまで、②、③を繰り返す

⑤(審査が通った後、)リリースブランチのマージ
 githubのマージ機能、gitコマンドの"merge", "rebase"など使用する
 →自分のケースだとどれが良いかは、別途整理します。

④(審査が通った後、)ブランチのプッシュ
 プッシュ対象ブランチ:master, develop

⑥リリースタグの作成
 githubで"Draft a new release"を使用し、リリース時点のタグを作成する

おわりに

記事を書いている最中に"Github-flow"という別のgitのブランチ戦略があることを知りました。
一旦は、今回の"git-flow"をベースに開発を進めて、「なんとなくブランチを切る」という開発にさよならしていきたいと思います。

【swift】AVFoundationを使って、録音、動画の撮影を行う!

はじめに

最近、CoreMLという「学習済み機械学習モデル」を簡単にiOSに実装できるという技術に興味を持っております。
中でもVisionAPIを使用すれば、画像、動画から人間の骨格や特定の動作を検出するといったことの再現が可能になります。

私たちはスマホのカメラアプリを使って写真や動画を撮影することがありますが、CoreMLの技術を搭載することで今まで見えなかったものも見えるようになると考えています(すごい!)。

CoreMLは音声や文章、画像などを解析するAPIを持っているのですが、前提としてこれらのAPIには「インプットとなる情報」が渡されています。

ですので、この「インプットとなる情報」を収集するロジックについても、最低限の実装方法は理解する必要があると考えております。


今回の記事では「AVFoundation」を使用して、そのインプットとなる情報(画像、動画)を取得する方法について概念をまとめます。

AVFoundationについて

参考

本記事では、以下のドキュメントを参考にしております。
developer.apple.com

概念

本ライブラリを使用すると、
iPhoneのカメラやマイクを起動して「音声」や「画像」といった「入力データ」として取得したデータを、必要に応じて加工したり保存する(アウトプット)ことが可能になります。

このAVFoudationのポイントは、「インプット」と「アウトプット」、そしてそれらを管理する「セッション」になります。
以下画像はCameras and Media Captureから引用したものですが、まずはこの図について理解していく必要があります。

https://docs-assets.developer.apple.com/published/058e665a6c/4ecf0924-ea2b-4faa-aea8-7bfc0b3fe419.png

セッションの構成要素

上記画像にも記載してあますが、AVFoundationを使用するために必要な要素は以下三つになります。
・AVCaptureInput
・AVCaptureOutput
・AVCaptureSession

「AVCaptureInput」、「AVCaptureOutput」の実態はただのabstractクラスでして、インプット、アウトプットに必要な変数・メソッドが定義されています。
そして、このabstractクラスを実装している「インプット」、「アウトプット」クラスはいろんな形で用意されています。

例えば、「AVCaptureInput」を継承する「AVCaptureDevice」クラスは、「カメラ」と「マイク」を起動して、音声つきの動画の撮影が可能となります。
→詳細は後述しますが、AVCaptureInputは「Port」変数を所持していて、この「Port」に対してインプットの情報を格納していきます。

次に、アウトプットとして「AVCaptureOutput」を継承したクラスを用いて、インプットで取得したデータをどう出力するか?設定します。
「AVCaptureMovieFileOutput」では音声つきの動画を「.mov」ファイルとして保存しますが、「AVCaptureAudioFileOutput」では「音声」のみを指定した拡張子に変換して保存します。


最後に「AVCaptureSession」、これは上記のインプット、アウトプットに関する「セッション」を制御していく役目を担っていて、上記で説明したようなインプットとアウトプットの紐付けや、動画撮影などの「開始・終了タイミング」を制御をします。


まとめです。

AVFoundationでは、「AVCaptureInput」によって動画や音声を記録することができますが、それを最終的にどう扱いたいか目的に応じて「AVCaptureOutput」を選択する必要がある。
また、AVCaptureSessionでは「AVCaptureInput」と「AVCaptureOutput」に対するセッション管理などを担っている。

インプットに関わる「ポート」の説明

概念

先ほど「ポート」について軽く触れましたが、ポートは「どの端子でどういったデータを取得するか?」を定義するもの、と考えれば良いのかなと思います。

以下コードは、「AVCaptureInput」を継承したクラス(AVCaptureDevice)に対して、ポートを定義する方法を示します。

// ポートの定義
guard let captureDevice = AVCaptureDevice.default(.builtInDualCamera, for: AVMediaType.video, position: .front) else {
        return
}
// AVCaptureDeviceInputクラスの定義(ポートを設定している)
guard  let videoInput = try? AVCaptureDeviceInput(device: captureDevice) else {
    return
}
// portsメソッド(AVCaptureInputクラスのabstractメソッド)でも、ポート設定が可能
videoInput.ports(for: AVMediaType.video, sourceDeviceType: AVCaptureDevice.DeviceType.builtInDualCamera, sourceDevicePosition: AVCaptureDevice.Position.back)

上記のコードで何となくイメージが掴めると思いますが、「AVCaptureInput」に対して一つポートを設定しています。

AVMediaType.video 動画で撮影することの定義
AVCaptureDevice.DeviceType.builtInTripleCamera トリプルカメラを使用することの定義
AVCaptureDevice.Position.back 背面カメラを使用することの定義


ちなみに、「AVCaptureInput」は、一つ以上の「ポート」の設定が可能です。
「音声付きで動画を撮影したい」というケースには、「カメラ用のポート」と「マイク用のポート」の二つを用意してあげないといけない、ということになりますね。


※余談ですが、Portのドキュメントのページには「Data Stream」という単語があり、直訳すると「データの流れ」ということでなかなかイメージしずらかったです。
ですので、私は「Data Stream = "どの端子でどういったデータを取得するか?"を定義するもの」と変換して理解しました。

Apple Developer Documentation

最後に

さて、今回はざっくりとAVFoundationについて説明しましたが、今現在適当なサンプルコードも作成しているところです。
実際にコードを書いてみて、気づきがあれば本記事をアップデート行こうと思います。


ここまで見ていただきありがとうございました!🙏

【Firebase】匿名ログインに必要な処理と考え

はじめに

本日はFirebaseのAuth機能における「匿名ログイン」についてまとめようと思います。

私のアプリ「Myトレ」では、アプリ起動時に「匿名ユーザ」にてログインするようになっています。
その後、「アカウント作成」により「匿名ユーザ」「永久ユーザ」に昇格することが可能となります。

永久ユーザにすることで「別の端末でログインしてもデータ引き継ぎ可能!」といった仕組みが再現可能になります(今となっては当たり前の仕組み!)。

この「匿名ユーザ」でのログイン機能は大変便利なのですが、運用の仕方によっては別途追加で機能を実装しないといけないいけないケースも発生します。
ですので、本記事では「匿名ログイン」についての機能を紹介し、気をつけないといけない点についてまとめようと思います。

前提

認証方法は「authUI」を使用しています。
後述の匿名ユーザ→本ユーザへの更新の仕方は、authUIを用いた方法に関して説明していますのでご了承ください。

FirebaseUI で iOS アプリに簡単にログインを追加する

匿名ログインについて

概念

Firebase で認証する一時的な匿名アカウントを、Firebase Authentication で作成して使用できます。一時的な匿名アカウントを使用すると、アプリに登録していないユーザーが、セキュリティ ルールで保護されているデータを使用できるようになります。

iOS で Firebase 匿名認証を行う

アプリによっては起動直後に「アカウント登録してください!」とメッセージが出て、登録しないとサービスを使えないようなものあるかと思います。
「匿名ログイン」することで、強制的なアカウント登録の手続きをせずに、アプリを使用すること(CloudStoreへのデータ登録)が可能になる、といったメリットがあげられます。

コード

匿名アカウントでのログイン処理
import Firebase

// 匿名ユーザログイン処理
Auth.auth().signInAnonymously() { (authResult, error) in
    // エラーチェック
    if let err = error {
        
    }
            
    // ログ出力
    let isAnonymous = authResult?.user.isAnonymous  // true
    debugLog("isAnonymous: \(isAnonymous as Any)")
    debugLog("uid: \(authResult?.user.uid as Any)")
    debugLog("email: \(authResult?.user.email as Any)")
    debugLog("photoURL: \(authResult?.user.photoURL as Any)")
}

コード量はこんだけです。簡単ですね!

匿名アカウントから永久ユーザへの昇格(authUIでのログイン方法)
import Firebase

class viewController: UIViewController {
    var authUI: FUIAuth!

    // 認証に使用するプロバイダの選択
    var providers: [FUIAuthProvider] = [
            FUIGoogleAuth(),
        ]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.authUI.providers = providers
        self.authUI.delegate = self
        <b>authUI.shouldAutoUpgradeAnonymousUsers = true   // 匿名、永久への変換を可能とする</b>
    }

    // ログインボタン(authUI画面を表示する)
    @objc func closeTupped(_ sender: UITapGestureRecognizer) {

        let authViewController = self..authUI.authViewController()
        authViewController.modalPresentationStyle = .overCurrentContext
        self.show(authViewController, sender: nil)
    }

}

// delegate
extension viewController: FUIAuthDelegate{
    // 認証処理
    public func authUI(_ authUI: FUIAuth, didSignInWith user: User?, error: Error?){
        // 処理は省略
    }
}

ポイントは「authUI.shouldAutoUpgradeAnonymousUsers = true」だけです。
このコードを追記した上でauthUI画面を立ち上げログインすると、匿名アカウントを永久アカウントに昇格することができます。

認証時に「成功」、「失敗」するケース

認証する際のアプリの状態やログイン先のアカウントの状態といった、それぞれの状態のケースで認証試みるとどうなるのか?以下にまとめました。

アプリの状態:ログイン時、未ログイン時
アカウントの状態:永久アカウントのユーザ、未登録のユーザ

アプリの状態 永久アカウントのユーザ 未登録のユーザ
未ログイン時 成功:新規ユーザに対してログイン 成功:新規アカウント作成
匿名アカウントでログイン時 失敗 成功:新規アカウント作成(昇格)

上記の表で書いてある通り、「匿名アカウントでログインしている際」に、「既に存在するアカウント(一度認証済みのアカウント)に対してログインする」とエラーが発生します

ですので、認証済みのアカウントにログインする際には、未ログイン状態にしてあげる必要があります

小話 - 匿名アカウントでログアウト

してしまうと、そのアカウントに対して再度ログインができなくなってしまいます。

ですので、匿名ユーザでしかアクセスできないデータがCloudStoreに残ってしまうと、このデータはアプリから削除する術がなくなってしまいます。


と、私も今更気づいた次第です。

ならば、匿名ユーザでログアウトする際に、該当ドキュメント配下一式を削除すればいいじゃん!と思った次第ですが、
該当ドキュメントを消して配下のフィールド、コレクション一式も削除するといった操作はできないそうです。

★ドキュメント配下のコレクションを一つ一つ消していけばドキュメントも削除できるようになるのですが、処理に時間がかかる(たくさんコレクションを生成している場合など)こともあり自作バッチによる削除を推奨しているようです。

https://firebase.google.com/docs/firestore/manage-data/delete-data?hl=ja#python_2


さてさてどうしようか、ということで次回Firebaseのバッチ処理辺でお会いしましょう!笑

最後に

最近朝起きれなさすぎて光目覚まし時計を購入しました。
これで朝型人間になるんだ・・・!

【Firebase】ドキュメントの更新

本日はFirebaseのCloud Storeにおけるデータの更新の仕方をまとめようと思います。

自分のアプリの品質強化施策で、ドキュメント更新失敗時のケースを検討しようとソースを見てたのですが、パッと見て実装に問題があるのかわかりませんでした・・・
→実装の仕方をすっかり忘れてた!

ですので、復習がてらドキュメントの更新方法に関して、swiftでの実装方法を紹介しつつまとめてみようと思います。


以下サイトを参考に実装していますが、基本的には自分の使用した機能を紹介します。

firebase.google.com

前提

Cloud Storeを使用するにあたって、「コレクション」、「ドキュメント」、「フィールド」といったデータに関する概念の説明は省略します。

最初に - フィールドに格納できる情報について

文字列、ブール値、日付、NULL、ネストされた配列、オブジェクトなどを格納できます。

以下サイトにて使用できる型がまとまってます。
サポートされているデータ型  |  Firebase

Myトレの例

せっかくですので、私のアプリにおける「日々の記録」のデータ構造をご紹介します。
以下は実際にMyトレで、Cloud Storeにデータを格納する際に定義している変数になります。

フィールドに投入するデータは、swift上では[String: Any]型である必要があります。

実際にCloud Storeで管理する際には「map型」で管理するため[String: Any]型と言いつつも、intやtimestamp型が登場しています。

let docData: [String: Any] = [
    "taskData": [
        "腕立て伏せ": [
            "1": [
              "count": 15,  // Int型
              "date": Timestamp(date: "date型、作成した時刻") 
            ],
            "2": [
              "count": 17,  // Int型
              "date": Timestamp(date: "date型、作成した時刻") 
            ]
        ],
        "スクワット": [
          "1": [
            "count": 20,  // Int型
            "date": Timestamp(date: "date型、作成した時刻") 
          ]
       ]
    ]
]

コード実装

コレクション・ドキュメントの定義

以下のコードでが、実際にデータを格納したいコレクション、ドキュメントを定義しています。
後述しますが、以下定義した変数からsetDataなどのメソッドを呼び出し引数に上記の[String:Any]型の変数を設定することで、Cloud Storeにデータが格納できます。

import Firebase

var baseRef: DocumentReference? = nil
var baseCol: CollectionReference

baseCol = Firestore.firestore().collection("ルートで、すでに存在する、これから作るコレクション名を定義")
baseRef = baseCol.document("すでに存在する、これから作るドキュメント名を定義")

ドキュメントの作成

ドキュメントを新規に生成する場合は、以下の3つのパターンのどれかで実装します。

ドキュメント名を自分で定義する場合
baseCol.document("20201102").setData(docData)
ドキュメント名を自動で定義する場合
baseCol.addDocument(data: docData)
すでにドキュメントが存在する場合の対応
baseCol.document("20201102").setData(docData, merge: [true or false])

すでにドキュメントが存在する場合にsetDataをすると、新しいドキュメントで上書きされてしまいます(既存のドキュメント・フィールドが消える)。
mergeオプションをtrueにすることで、既存のデータを消さずに新しいデータを統合することが可能となります。

ドキュメントを更新する

以下の例は、上記のsetDataで投入したデータの一部を更新します。

let docUpdateData: [String: Any] = [
    "taskData": [
        "スクワット": [
          1: [
            count: 25,
            date: 20201202 17:35:30
          ]
    ]
]

baseCol.updateData(docUpdateData) { err in
    if let err = err {
        print("Error updating document: \(err)")
    } else {
        print("Document successfully updated")
    }
}

補足
updateData = setData(data, merge: true)なのかなと思ってます。実際に検証したことがないので、挙動に差異があるか機会があれば別途調べてみようと思います。
ですが基本的には、初回のデータ投入:setData、それ以降のデータ更新:updateData、という考えで問題ないと思います。

最後に

データの更新自体は、上記程度のコード量で済んでしまうのでぶっちゃけ楽です!
私は、最初に実装したのはもう一年くらいになりますが、初めてデータを投入できたときは感動しました。

おおー、データ投入できた!!!ってめちゃめちゃ喜んでました笑

ただ、どういった構造にしていくか?検討するフェーズが難関であると思います。
なんやかんやで1ヶ月くらいはトライアンドエラーで実装検証してました・・・

また、セキュリティルールも同時に検討していかないと、大事な個人データが勝手に流出されてしまうかもです。
→セキュリティに関しては、次回別途何か記事を作ろうかなと思ってます!


そんなわけで、ドキュメントの更新方法についてでした!

【BDD, TDD】テストコードを書いていて感じたこと その1

こんにちは、日に日に寒くなってきましたね。
ふと思えば本日は11/28で今年も残すところあと1ヶ月になります。

このブログでは「ドラムと筋肉とプログラミング」ということで、それぞれについて思うことを発信して行こうと思ってたのですが、気づけば「プログラミング」の記事がメインとなりつつあります。。。

とはいえ、筋トレは自作アプリにより自重トレーニングが継続できるようになりましたし(自分の胸筋好きになった!)、ドラムも昨年よりスキルが上がったと思います。

今年も残り限られてますが、自分の体に労わりつつ、新たな気付きを得られるよう行動していきたい、そんなふうに感じた今日この頃です。

はじめに

さて、ということで相変わらず?プログラミングのお話です。
前回の記事でBDD, TDDについて触れ、テストコードを書き始める前に自分のソースで気づいた点をまとめました。

BDD, TDDを始めたいです・・・ - ドラムと筋肉とプログラミング


「内部の振る舞いが理解できていない」という現状があったので、現在「仕様」に対する振る舞いも整理していて、その上でテストコードを追加しております。
この作業をする上で、改めてインターフェースを先に実装していくと言った考えが特に重要だなと感じた次第です。

本日の記事では、新たに機能を追加する際に、インターフェース(プロトコル)から実装していく必要があると思いますが、どう言った観点で実装していくべきかまとめようと思います。
→ぶっちゃけ前回の記事の言い方変えただけ!でもありますが笑

結論

新しい機能(仕様)を追加する際に、以下に関するインターフェースから検討・実装する

  1. 仕様に対して、実装する見込みのある「すべてのソース」
  2. 1.に対して、異常系を見込む(DB接続、外部APIへのリクエスト送信など)必要のある「ソース」


整備中・・・

理由

インターフェースから実装するということは、「仕様の骨子を設計する」と言い換えることもできると考えています。

この骨子を設計することは、プログラムの「全体像」が見えるタイミングでもあるため、この時点でメソッドに対する実装は絶対にしてはなりません!!


私は欲しい機能に対して全体の骨子の実装から始めていなく、基本的に正常系のケースしか考慮していませんでした。
後に異常系も検討して行こうと思っても、正常系にのみ対応した構造になってしまっているため、再度「骨子(インターフェース)から見直さないといけない状況」になっていました。

家を建てるという建築の話に例えると、家の骨子(柱)から作り始めずに壁の塗装や階段を実装し始めてしまったので、後で思う位置に部屋を追加できなくなってしまう、って感じでしょうか(適当)。

プログラムは家と違ってやり直しが効くので、後から骨子を見直すこともできますがそれだけ時間が必要になってしまって無駄です。

ついでに思ったこと - シーケンス図ってやっぱ大事だよね

振る舞いを明確にするために同時に「シーケンス図」があると超いいな!と感じた次第です。

最近アジャイル開発などで設計書はいらん!みたいな話がありますが、私はこのシーケンス図は絶対にあったほうが良いと考えてます。
特に、仕様が多岐のクラスに跨いで振舞うことで、コードを追いかけるのが面倒くさくなります。
→私はアジャイル開発したことないので現状はよくわかってないですが・・・

こう言ったときに、どんなクラスに影響するのか?一つの図で表現できると振る舞いに対する理解が圧倒的に早くなると思います。


自分はシーケンスまで作れてないのですが(さぼっていて)、シーケンスを自動化できるツールがあったら是非導入したいです。
ちょいとググってみますか・・・!

最後に

言い訳ですが、概念に対して「例」をすぐに出すのって難しいです・・・
自分の考えをまとめる(発進する)機会を増やして、こう言った例題もパッと出せるようにしたいです。