2020年の目標

2020年の目標

2020年は主に以下の領域に注力します。

  • データ構造とアルゴリズムの理解の向上
  • プロダクト作成の基礎力向上
  • アウトプット
  • 身体能力の強化

各目標は上記の注力領域を達成するように作成しています。

データ構造とアルゴリズム

プログラミング問題

  • 年次目標 300問
  • 月次目標 25問

データ構造とアルゴリズムの理解力向上を目的にプログラミング問題を300問解きます。 Easy: 150問、 Medium 125問、 Hard 25問くらいの割合を想定していますが、この点は状況によって変動します。

世界で闘うプログラミング力を鍛える本 通読

  • 年次目標 通読・問題全問正解

「世界で闘うプログラミング力を鍛える本」を通読し、問題を全て解くことを目標とします。本目標で解いた問題数は「プログラミング問題」の回答数にも含めることとします。

プロダクト作成の基礎力向上

読書

  • 年次目標 12冊
  • 月次目標 1冊

今後も風化する可能性の少ないような本を中心に書籍を読み進めます。具体的には下記のような本を指します。

アウトプット

ブログ投稿

  • 年次目標 50
  • 月次目標 約4

1投稿 / 週を目標にブログ記事の投稿を行います。内容は主にモバイル関連技術領域(iOS, Android, Flutter)としますが、本の感想や目標進捗管理も含みます。

身体能力の強化

ウェイトトレーニング

昨年の目標が未達だったため、2020年に目標を持ち越します。

体脂肪率 10%

体脂肪率を10%周辺まで低下させます。

日々の学習の方針

直近2 - 3年は以下の技術セットを有することを目指し、学習を行います。

モバイル領域のスペシャリスト

  • iOS, Android, Flutter 全てに対してアーキテクチャ設計を適切に行い、長期的な保守を見据えた設計・実装及び技術選定が可能
    • 優先順位 iOS > Android > Flutter 業務内容に準じて変動します
  • Firebaseの各機能に関して精通し使いこなすことができる
    • 開発の分野を特に重点を置く。(アナリティクス、品質、拡大は経験し、ある程度使用できる)
    • Firebase Extensionを状況に応じて使用できるようにする
  • モバイルをクライアントとした際の適切な全体設計に関して議論できる
    • 適切なAPI設計を行うことができる
    • 状況に応じてインフラ設計の議論を行える
      • Firebaseとするか自社サーバーとするか
      • DBへのデータの格納の仕方
      • ログの取得方法
      • etc

他の項目について

上記モバイル関連領域に必要に応じて下記を学習します。

  • 機械学習 - 状況によっては注力の度合いが変わります。その場合は本項目に関して年次の定量目標に関連項目を追加します。
  • Ruby On Rails - サーバーサイドの設計理解を目指します。

さいごに

今年の個人的なテーマは技術的な貯蓄を作るです。今後数年間に渡って活躍する上での基本となる土台をしっかりと作ることを目標とします。そのためにデータ構造・アルゴリズムや設計思想、コンピュータ科学など時代を通じて風化しづらい知識・技術の習得を中心に据えて活動を行います。

2019年の振り返り

2019年の振り返り

通年の振り返りです。まずは目標の達成状況の確認から行います。

体の基礎づくり

達成率 33% 未達。

アウトプット

ブログ投稿

  • 目標: 100記事 /年 -> 70記事 / 年 -> 50記事/年
  • 実績: 今月 - 本記事も合わせて5記事 / 通年 - 53記事

達成率 100% (下方修正前 53%) 達成。  

登壇

  • 目標: 6件 / 年 -> 3件 / 年 
  • 実績: 今月 - 0件 / 通年 - 全2件

達成率 66%。未達。

サービス開発(アプリに限定しない )

  • 目標: 3件
  • 実績: 今月 - 0件 / 通年 - 1件

達成率 33%。未達。

スキルアップ

AtCoder

  • 目標: 水色
  • 実績: 茶色

未達。

LeetCode

  • 目標 50問 -> 25問
  • 実績 今月 - 25問 / 通年 - 35問

達成率 下方修正後 100%( 下方修正前 70%) 達成。

Git

  • 目標: 500 commit / 年
  • 実績: 17 commit / 月
  • 実績: 270 commit / 年

達成率 54% 未達。

その他

読書冊数

2019/9 ~ 2019/12の期間で 17冊。おおよそ 4冊 / 月。他の期間に関しては記録なし。

参加カンファレンス数

2019/8 ~ 2019/12の期間で11。他の期間に関しては記録なし。自身の知識の裾野を広げることを目的にDesignやProductManagerのカンファレンスなど普段の業務とは少し違う内容のものにも積極的に参加するようにしました。

年初に定めた方向性と実際の比較

年初に目標を定めた際に以下を一年の方向性として定めました。結果どうであったかを振り返りながら検証していきます。

資産形成はじめの一歩

少額ですができました。iDeCoなどをはじめ、少額ではありますが積み立てを開始できています。来年は上記の積み立て以外に積み立てNISAへの拠出と株式購入をいくつか行いたいと考えています。

体の基礎作り

ベンチプレス 70 -> 100、デッドリフト 100 -> 120、スクワット 100 -> 120なので、順調に成長できたのではないかと感じています。来年は目標としている100, 150, 150の数字に到達できるように努力を続けていきたいと考えています。

アウトプット重視

ブログ投稿を継続できたため、この点も達成できたのではないかと感じています。GitHubでのコーディング活動や外部講演があまりできなかったため、その点は改善点です。

収入の多角化

これはあまり進んでいるとは言い難いです。いわゆる給与収入以外の収益源を作りたかったのですが、なかなか難しいですね。

スキルアップ

これは達成できたと感じています。退職し、新しいドメイン領域の会社に移ったこともあり、今まで携わった経験があまりなかった技術要素に取り組むことができました。Android開発に着手し、開発可能な領域が広がったのも良かったと思う点です。

以上、おおよそ年始に描いていた方向に進めているなという状況です。至らなかった点もありますが、おおよそ良かったのではないでしょうか。

通年を通しての感想

今年の大きな変化として正社員から個人事業主へと働き方を切り替えたことがあります。正社員と個人事業主どちらがよりよい働き方かは個人個人と状況によって変わりますが、このタイミングで個人事業主として働くという選択をしたことは良い判断だったのではないかと考えています。まだ確定申告の洗礼を受けていないので、その点は不安もありますが、きっとなんとかなるとも感じています。

個人事業主として働くことを検討した際に主に以下の目的がありました。全て達成することができたと感じています。

反面、個人事業主にない正社員のメリットも意識するようになりました。雇用の安定性や保険料の折半などもありますが、事業そのものの意思決定に深く関わろうとした場合、やはり個人事業主では限界がある点は否めないなと感じています。

自身が働く上で何にこだわるかという点もありますが、自身の今後のキャリアプランも鑑み、今後も個人事業主を続けるか、正社員となるか、それらの掛け合わせで働くか、考えていこうと思います。

また、このブログを開設し、記事を定期的に更新するという活動を通年続けることができたのは大きな励みになりました。なかなか何を書いて良いか戸惑うこともありましたが、記事を投稿し続ける中で勘所もつかめ、抵抗のような物が徐々に減ってきました。続けることそれ自体が大切になることもあるのだと実感しています。

良いことも悪いこともありましたが、総じて得る物が多かった一年であったと感じています。関わっていただいた方々に感謝します。1年間ありがとうございました。

2019年12月振り返り

12月の振り返り

目標の進捗状況の定期確認です。12月は数年ぶりに冬服を一通り買い換えました。体のサイズにあわなくなってきたことが一番の理由でしたが、普段きている物を変えるのは良い気分転換にもなりました。衣服は良くも悪くも自身の心持ちに影響を与えるなと感じたので、これからは定期的にその時にあった服に買い替えを行おうと思います。

体の基礎づくり

こちらは特に変わりありません。最近あまりジムに行けていないので、どうにか時間をとりたいところです。 通年の目標はベンチプレスのみ達成、デッドリフトとスクワットはあと一歩及ばずでした。

アウトプット

ブログ投稿

  • 目標: 100記事 /年 -> 70記事 / 年 -> 50記事/年
  • 実績: 今月 - 本記事も合わせて5記事 / 通年 - 53記事

下方修正後ではありますが、通年の目標を達成しました。  

登壇

  • 目標: 6件 / 年 -> 3件 / 年 
  • 実績: 今月 - 0件 / 通年 - 全2件

今月は特に登壇は行なっていません。 通年目標には到達できずでした。

サービス開発(アプリに限定しない )

  • 目標: 3件
  • 実績: 今月 - 0件 / 通年 - 1件

特に進捗ありません。こちらも到達できずです。

スキルアップ

AtCoder

  • 目標: 水色
  • 実績: 茶色

動けていません。

LeetCode

  • 目標 50問 -> 25問
  • 実績 今月 - 25問 / 通年 - 35問

12月は問題を解くことに集中していました。 動的計画法、バイナリツリー、バイナリサーチ、BFS、DFS等の基本はある程度おさえられたのではないかと感じています。下方修正後ですが、通年の目標を達成しました。

Git

  • 目標: 500 commit / 年
  • 実績: 17 commit / 月
  • 実績: 270 commit / 年

こちらはあまり進捗を出せませんでした。 通年の目標未達です。

その他

活動

読了

読書開始

読書中

ユニットテストについての少考

はじめに

今年読んだ以下の本の中で設計におけるユニットテストの立ち位置を語る箇所が多くみられました。「レガシーソフトウェアからの脱却」、「レガシーコード改善ガイド」、「CAREER SKILLS」、「クリーンアーキテクチャ」など。実際に仕事の中で使ったり、考察を行う中で自分なりにユニットテストの有用性や実務での妥当な利用方法を考察しました。

本稿はそれらを踏まえた上での現状の私の考えです。業界のスタンダードからは認識がずれている可能性がありますし、基本的には自身の思考を整理するための手段として本稿の文章を書いています。

ユニットテストの効用

ユニットテストの効用は大きくはおそらく以下の3点です。

リグレッションの防止

ユニットテストを書くことによってリグレッションが発生した際に気づくことができます。例えば既存クラスになんらかの変更を加える際に、 特定の条件でtrueと返却するはずが、その変更によってfalseが返却されるようになっていた場合、既存の処理に悪影響を及ぼしていることになりますが、該当処理にユニットテストを作成していればこのことに気づくことができます。

また、ユニットテストによるリグレッションの防止が達成されている場合、リファクタリングをより容易に行うこともできるようになります。リファクタリングふるまいを変えずに内部処理をわかりやすくしたり、効率的にすることとします。ここでふるまいを変えずに、という点がポイントになります。

ユニットテストは該当箇所のふるまいをテストできます。よって、ユニットテストがある状態でリファクタリングを行い、ユニットテストが問題なく通った場合はリファクタリングによってふるまいが変わっていないことが保証できるため安心して効率的に作業を行うことができます。

リグレッションを防止する、もってリファクタリングの効率性を向上する、これがユニットテストの1つ目の有用性であると考えています。

ドキュメンテーションとしての役割

ユニットテストは対象のメソッドに対するふるまいの仕様を表すドキュメンテーションとして機能します。エッジケースの際にどのように振舞うのか、発生しうる分岐パターンは何か、そういったことがユニットテストから把握することができます。

コードから独立しているドキュメンテーションは時間の経過とともに実コードと解離が発生しがちですが、コードの作成とともに実装、修正されるユニットテストであればコードからの解離は原理上発生しづらく、有効なドキュメンテーションとして機能します。

内部構造の洗練化

ユニットテストを実装できるようにコードを実装する行為によって内部構造が洗練化されます。ユニットテストを実装できるようにするためには、クラス間が疎結合であり、影響の範囲が限定されていなければなりません。また、必要のない場合は外部にインターフェースを公開せず、該当処理や状態を内部に閉じ込める必要があります。必要な依存関係に関しては依存関係の注入をできるようにすることでモックと入れ替えられるようにする必要もあります。なぜならば、そうしなければ依存先のコードの実装に依拠してしまい、ユニットテストにならないためです。

CLEANとSOLID原則という言葉があります。CLEANはコードレベルでの指針を指し、Condensed, Loosely Coupled, Encapsulated, Assertive, Not redundantの頭文字をとった言葉です。(記憶がおぼろげなので各英単語は間違っているかもしれません)。凝縮されており、疎結合カプセル化され、冗長性がなく、断定的であることをもって運用・保守性の高いコードであるという内容だったかと思います。

SOLID原則はアーキテクチャレベルの内容です。 SRP(Single Responsibility Principle - 単一責務の原則)、 OCP(Open Close Principle - 海保閉鎖の原則)、LSP(リスコフの置換原則)、ISP(インターフェース分離の原則)、DIP(依存関係逆転の原則)を指します。

ユニットテストを作成しやすいようにアプリケーションを作成していくと上記のCLEANでSOLID原則に則ったコードになりやすいと考えています。ユニットテストを書きやすくするためには各ユニットに対して依存関係を少なく、テストが必要なインターフェースを少なく、非冗長なコードにする必要があるためです。

ユニットテストを実装しながらプロダクトコードを書くことで、CLEANでSOLID原則に沿ったコードを作成しやすくすることができると考えます。

アプリ開発でどのように活かすか

アプリ開発で上記の効用を全て得るための方法を考えます。おそらく新規のアプリ開発と既存アプリへの適用で方法は変わります。本稿では新規アプリ開発に焦点を当てます。

効用全てを得るためには、テストをアプリのコードを作成してから作成するのではなく、アプリのコード作成と並行してユニットテストも作成すると良いのではないかと考えています。具体的には、アプリ側でふるまいとしてInterfaceを切る、そのInterfaceに対してユニットテストで期待するふるまいを実装する、テストに通るようにアプリ側のコードを埋めるというような流れです。いわゆるTDDの方法をとるのがユニットテストの効用を得るには最善なのではないかと考えています。(BDD Behavior Driven Developmentという言葉もあるようですが、こちらはあまり詳しくありません)

ただ、毎回全て手動で作成するのは大変なためGenerambaのようなコードジェネレーションツールを使用して、アプリのコードと同時にユニットテスト用のガワを作成するのが良いと思います。MVVM、 Viper、 Reduxなどのアーキテクチャに沿って、必要となるコードのセットを作成するようにテンプレートを作成するといった方法です。

このようにすることでコードジェネレーションツールを使用するかぎりにおいて、実装とユニットテストの作成を同時に進めることを半強制することができ、ユニットテストの効用を全て得ることができるのではないでしょうか。

とはいえ

上記までの考えが今現在考えているユニットテストのメリットを得るための開発方法ですが、とはいえなかなか大変でもあると考えています。また、全ての開発をこの方法で行うべきとも考えていません。PoC段階のアプリでそのアプリベースから発展させることを意図していない場合はNoCodeでさくっと作ってマーケットの反応を見た方が良いでしょうし、メンバーのレベルによってもとりうる方法が変わります。

新規開発であれば上記の方法で良いでしょうが、既存の大量のコードがある状態からのユニットテストの導入をする場合ではこうもいきません。(この辺りはレガシーソフトウェア改善ガイドが大変参考になりました)。結局のところ都度都度状況に応じた最適解を選択できるよう考える必要はあるかと思います。

最後に

現状の自分の考えをまとめてみました。あくまで今はこう考えるというものであり、今後の経験やより多くの知識を得る中で変わることもあるかと思います。また絶対的に正しい考え方だとは思っていません(往々にして正しさは適用する対象と状況に応じて変化しえます)。最近得た知識をまとめるために記載しましたが、これはこうなんじゃないか、といったご意見がありましたらコメントいただけますと幸いです。

「会計の世界史 イタリア、イギリス、アメリカ――500年の物語」の感想

読んだ本

会計の世界史 イタリア、イギリス、アメリカ――500年の物語

会計の世界史 イタリア、イギリス、アメリカ――500年の物語

本書は会計に関して、エピソードを交え歴史的な経緯を追うようにして書かれた作品です。一通り読み終えたので、記憶の定着をもく定期として記述します。記憶を辿りながら記述するため記載には誤りがある可能性があります。気づかれましたらご指摘ください。

財務会計管理会計ファイナンス

財務会計

ストレンジステークホルダー(外部の株式取得者)に対する報告のための会計。バランスシートや損益計算書およびキャッシュフローシートを作成する。よくIRで見る。 1500年頃のイタリアでは自身の儲けの計算のために帳簿が付けられていたが、外部に公開し、外部が会社の財務的健全性をみるという点でその頃の会計とは異なる。

管理会計

外部の説明ではなく、自社内で自社の状態を把握するために使用する。デュポンによる事業部制および 利益=利益率 * 回転率の公式に当て嵌め、各事業部の収益性を把握するためなどに使用する。

ファイナンス

20世紀後半以降 情報・人などB/Sに載らない資産が増えてきたことに伴い会社の価値把握をB/S, P/L, CFから判断するのは難しくなった。しかし M&Aの際などには会社の価値を把握する必要がある。この際に会社の将来キャッシュフローの各年の割引率をかけて現在価値を算出する方法が生まれた。これをコーポレートファイナンスという。

ファイナンスは将来価値から現在価値を算出するが、将来価値を高めるためにどのようなB/S, P/L, CFが良いかなどの今までに無かった観点が入る。したがってファイナンスの登場により、従来の財務会計にも影響がでた。減価償却費の減損扱いや、のれんの減損など。

なお、のれんは会社の取得価格と買収会社資産の差分に対応した仕訳だが、これはB/Sに計上される資産と情報・人材などB/S上に表現されない資産の差分に対応する。

B/S上の右下

物語の中でエジソンなどの偉人と彼らの起業した会社に関しての記載がでてくるが、B/Sの資本(株式)を握られたことで会社を追われている。資本主義においては会社は資本を握った人のものであり、資本の原理にのっとるとそうなる。

会計の論点

  • コストの算出
    • 事業部別に分けた際に減価償却費や人件費等をどのように配分するか
  • 時価か原価か
    • 取得時の価格で計上するか、売却する場合の価格で計上するか(INを主眼におくか、OUTを主眼におくか)

基準による利益の違い

どの会計基準を用いるかで最終的に計上される利益が大きく異なる。日本は日本GAAP、 US GAAP、IFARSが混在しているらしい。国際会計基準はIFARS(International Finance Accounting Reporting Standard)。アクチュアリーの勉強の際に見た気がする。

関係性

  • 財務会計は会計の過去の結果を記録し外部のためにつける。
  • 管理会計は現在の状態の把握のために自身のためにつける。
  • ファイナンスは未来の予測から現在価値を算出する。

歴史

感想

得てすると数字の羅列として見えてしまい無味乾燥になりがちな会計という分野を歴史的なエピソードを交え大変楽しく学べる良著でした。内容もさることながら調査範囲の膨大さには目をみはる物がありました。巻末の参考文献を見ると、それだけで本書全体のページ数の5%にあたりました(kindleの表示を参照)。

これだけの膨大な内容を物語形式でまとめるのは並大抵のことではないと思います。大変楽しい時間を過ごすことができました。著者の田中靖浩氏に感謝します。

【iOS】Firebase RemoteConfig で作成する強制ダイアログ機構

はじめに

Firebase Advent Calendar 12日目です。本稿では、Firebaseを使用した強制ダイアログ表示に関して記載します。ここで、強制ダイアログと表記しているのは、いわゆる強制バージョンアップダイアログに代表されるダイアログを表示し、それ以上のユーザーのアプリ使用を防ぐ機能を指しています。

具体的には、アプリケーションの起動時や復帰時にダイアログを表示し、アプリケーションのそれ以上の操作を行わないようにするものです。リリースしたアプリに致命的な不具合があった場合にそのバージョンを使用するユーザーを限りなく少なくするために強制バージョンアップダイアログを表示したり、サーバー側の何らかの不具合によりアプリ使用を防ぎたい場合に使用することが多いかと思います。

環境設定

以下の環境を使用しています。

  • Firebase RemoteConfig 6.12.0
  • Swift 5.1
  • Xcode 11.3
  • iOS 13.2.3

なお、本稿で記載している実装をまとめたサンプルを以下のようにGitHubに上げています。

イデアと実装

本章でどのようなアイデアに基づくのかと、具体的な実装に言及します。

概要

FirebaseのRemoteConfigを使用することで以下を管理します。

  • 対象アプリバージョン(本バージョン未満の場合にダイアログを表示する)
  • ダイアログのタイトル
  • ダイアログのメッセージ
  • ダイアログのOKボタン押下時の遷移先URL(App Store や Sorryページ)

それぞれを一項目として管理しても良いでしょうし、JSONでひとまとまりにしても良いかと思いますが本稿ではJSONとして格納した場合に関して記載します。

アプリ側ではFirebaseで設定されたバージョンと現在のアプリバージョンを比較し、現在のバージョンがFirebaseの設定バージョン未満だった場合にダイアログを表示するような機構を導入します。上記により、本稿が目的とする機構を実現することができます。

Firebase側の設定

Firebase側の設定に関して記載します。「概要」にても記載しました通り、本稿ではJSON形式でデータの登録を行います。パラメータは force_alert_information命名しました。行いたい挙動としては、該当バージョンアップ未満のアプリの場合にアラートを表示し、OKボタン押下でなんらかのWebページに遷移、ストアに遷移、もしくは何もせずにアラートを表示し続けた状態とするとします。これらを実現するために必要な情報は下記です。

  • title - アラートのタイトルに使用
  • message - アラートのメッセージに使用
  • version - アラートの表示可否で使用する基準バージョン
  • url - okボタン押下時の遷移先URL

実際に設定を行います。FirebaseのプロジェクトページのRemoteConfigからパラメータを追加することができます。 パラメータを追加 > 右側の {}を押下することでJSON形式でのパラメータ追加が可能になります。 f:id:Iganin:20191208215732p:plain f:id:Iganin:20191208215855p:plain

JSON形式でのパラメータ追加時は専用のEditorが表示され、JSON形式に添わない場合はエラーが表示され保存ができません。そのためJSON形式に添わない文字列を保存してしまい不具合が発生してしまう、というような事態は避けられるようになっています。

f:id:Iganin:20191208220051p:plain

RemoteConfigのWrapperクラスの作成

アプリ側でのRemoteConfigを扱うwrapperクラスを作成します。具体的な設定・導入までの手順は公式ドキュメントをご確認ください。本稿では、導入までは完了している前提で記載を進めます。

実装概要

以下に作成したクラスを記載します。コードにコメントとして直接詳細を記述していますが、主な注意点は下記です。

  • Debug時の設定
    • 実プロダクトで毎回fetchは取得上限回数に当たる可能性があるため厳しいですが、Debug時は即座に確認したいことが多いため、debugとreleaseで取得間隔を切り分けdebug時は0としています
  • 取得間隔
    • 1時間あたりの取得回数上限があるため、前回取得時より10分の間隔を設けています
  • デフォルト値の扱い
    • plistで扱う方法もあるかと思いますが、コード内で完結させたかったためenumでパラメータキーを定義し、デフォルト値を持たせています。
import Foundation
import FirebaseRemoteConfig

// 取得するパラメータを定義します
enum RemoteConfigParameterKey: String, CaseIterable {
    case forceAlertInformation = "force_alert_information"
    
    var defaultValue: NSObject? {
        switch self {
        case .forceAlertInformation: return ForceAlertInformation.defaultValue()
        }
    }
}

// RemoteConfigの設定用Protocolです
protocol RemoteConfigServiceProtocol {
    func fetchAllData()
}

// RemoteConfigのプロパティ取得用Protocolです
protocol RemoteConfigPropertyProvider {
    func getForceAlertInformation() -> ForceAlertInformation?
}

final class RemoteConfigService: RemoteConfigServiceProtocol {
    
    // Protocolを使用してDI時にモックとの入れ替えが可能なようにインスタンスとして扱うようにしています
    // Singletion
    static let shared = RemoteConfigService()
    
    private init() {
        remoteConfig = RemoteConfig.remoteConfig()
        // releaseビルドではない場合は取得感覚を0としています
        if !AppUtility.isRelease {
            remoteConfig.configSettings.minimumFetchInterval = 0.0
        }
        
        // 全てのデータを取得する前提で定義されているパラメータキーに対するデフォルト値を全て入れています
        remoteConfig.setDefaults(makeDefaultValues(forKeys: RemoteConfigParameterKey.allCases))
    }
    
    // MARK: - Prpoerty
    private let remoteConfig: RemoteConfig
    private var expirationDuration: TimeInterval {
        // debugビルドでは即時反映, releaseビルドでは一定時間あけるようにします
        switch AppUtility.buildType {
        case .debug: return 0.0
        case .release: return 10 * 60 // 10分間
        }
    }
    
    // MARK: - Function
    func fetchAllData() {
        remoteConfig.fetch(withExpirationDuration: expirationDuration) { [weak self] (fetchStatus, error) in
            guard error == nil else { return }
            
            switch fetchStatus {
            case .success: self?.remoteConfig.activate(completionHandler: { error in
                if let error = error {
                    print(error.localizedDescription)
                }
            })
            case .failure, .noFetchYet, .throttled: break
            @unknown default: break
            }
        }
    }
    
    // RemoteConfigParameterKeyで定義したKeyに対してデフォルト値を決定し代入します
    private func makeDefaultValues(forKeys keys: [RemoteConfigParameterKey]) -> [String: NSObject] {
        var defaultValues = [String: NSObject]()
        keys.forEach { key in
            // この部分は後ほど可能な限り修正します
            if let defaultValue = key.defaultValue {
                defaultValues[key.rawValue] = defaultValue
            }
        }
        return defaultValues
    }
}

extension RemoteConfigService: RemoteConfigPropertyProvider {
    private func getProperty(for key: RemoteConfigParameterKey) -> RemoteConfigValue? {
        return remoteConfig.configValue(forKey: key.rawValue)
    }
    
    func getForceAlertInformation() -> ForceAlertInformation? {
        // この部分は後ほど可能な限り修正します
        guard let data = getProperty(for: .forceAlertInformation)?.dataValue else { return nil }
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return try? decoder.decode(ForceAlertInformation.self, from: data)
    }
}

上記で使用しているForceAlertInformationの実装は下記です。

import Foundation

struct ForceAlertInformation: Codable {
    let title: String
    let message: String
    let version: String
    let url: URL?
}

extension ForceAlertInformation {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        message = try container.decode(String.self, forKey: .message)
        version = try container.decode(String.self, forKey: .version)
        let urlString = try? container.decode(String.self, forKey: .url)
        url = urlString == nil ? nil : URL(string: urlString!)
    }
}

extension ForceAlertInformation: RemoteConfigDefaultValueProvidable {
    static func defaultValue() -> NSObject? {
        let defaultValue = ForceAlertInformation(
            title: "確認",
            message: "新しいバージョンのアプリがあります。アップデートをお願いします。",
            version: "1.0.0",
            url: nil)
        let encoder = JSONEncoder()
        encoder.keyEncodingStrategy = .convertToSnakeCase
        guard let defaultData = try? encoder.encode(defaultValue) else { return nil }
        return NSData(data: defaultData)
    }
}

RemoteConfigの設定値の取得タイミング

applicationDidBecomeActive:のタイミングで取得するよう実装しています。このタイミングで行うことにより、アプリがタスクキルされていない場合でも background <-> forgroundの遷移で新しい値を取得・反映させることが可能です。

AppDelegate {
    func applicationDidBecomeActive(_ application: UIApplication) {
        // foreground <-> background遷移時に状態の変更を反映できるよう
        // このタイミングでRemoteConfigの設定値取得を行います
        RemoteConfigService.shared.fetchAllData()
    }
}

アプリ側の強制ダイアログ表示の実装

実装例

本稿のサンプルでは単純なMVC構成で作成しています。MVVMViperなど使用するアーキテクチャで具体的な実装は変わってくるかとは思いますが、その場合は初期表示される画面のViewModelPresenterで判定し表示を行うように実装するなどしていただけると良いかと思います。

バージョンの大小判定ロジック

Stringが比較演算を用いてそのまま比較可能なためバージョン判定時に下記のようにしてしまいがちです。

if currentVersion < criteriaVersion {
    // Alert表示
}

しかし、この方法ですと潜在的なバグを含んでしまいます。例えば、 currentVersion = "3.10.0"criteriaVersion = 3.9.0 の比較を行う場合本来であればcurrentVersionの方がcriteriaVersionよりも大きいためアラートが表示されて欲しくないですが、比較演算子で比較を行なってしまうと 3.10.0の方が3.9.0のより小さいと判定されアラートが表示されてしまいます。

これはおそらくですが、内部的に少数の比較となってしまっており、 3.1と3.9の比較が行われたことにより、3.10.0の方が3.9.0より小さいと判断されているために発生しています。そこで下記のように . で各桁の数字を分離し、各桁の数字の大小比較を行うことでバージョンの大小判定を行います。

struct VersionUtility {
    // 現在のバージョンと比較対象のバージョンを比べ強制アラートの表示が必要かを判定します
    func isForceAlertRequired(currentVersion: String, criteriaVersion: String) -> Bool {
        
        var currentVersionNumbers = currentVersion.components(separatedBy: ".").map { Int($0) ?? 0 }
        var criteriaVersionNumbers = criteriaVersion.components(separatedBy: ".").map { Int($0) ?? 0 }
        let countDifference = currentVersionNumbers.count - criteriaVersionNumbers.count
        
        // major versionから順に比較していき、同値でなくなった時に大小比較結果を返す
        for (current, criteria) in zip(currentVersionNumbers, criteriaVersionNumbers) {
            if current > criteria {
                return false
            } else if current < criteria {
                return true
            }
        }
        // 同値
        return false
    }
}

強制ダイアログの表示メソッド

OKボタン押下後にAlertが閉じないよう再度Alert表示のメソッドを呼び出しているところがポイントになります。

    func showForceAlert(information: ForceAlertInformation) {
        let alertController = UIAlertController(
            title: information.title,
            message: information.message,
            preferredStyle: .alert
        )
        let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in
            // okを押下後にAlertが非表示にならない用再度表示します
            self?.showForceAlert(information: information)
            if let url = information.url,
                UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            }
        }
            
        alertController.addAction(okAction)
        present(alertController, animated: true, completion: nil)
    }

実際の判定処理

ViewControllerで行う実際の判定処理部分です。実プロダクトで使用する場合は、アラート表示後の処理を行うかどうか検討し、Bool値を返すようなメソッドに分割変更した方が良いかと思います。

    func showForceAlertIfNeeded() {
        guard let forceAlertInformation = remoteConfigPropertyProvider.getForceAlertInformation() else { return }
        
        let currentVersion = AppUtility.currentVersion
        let criteriaVersion = forceAlertInformation.version
        if VersionUtility().isForceAlertRequired(
            currentVersion: currentVersion, criteriaVersion: criteriaVersion) {
            showForceAlert(information: forceAlertInformation)
        }
    }

タイミング

ダイアログの表示の必要有無の判定および表示処理を実施するタイミングとして下記のライフサイクルを採用しています。

初期表示される画面のviewDidLoad

サンプルでは初期に表示されるViewControllerのViewDidLoad内で処理を行なっています。他の方法としては、例えば、 LauchScreenの表示後にLaunchScreenと同様の画面をUIViewControllerで表示し、そのviewDidLoadで判定を実施の後、ダイアログを表示するかTabBarControllerを表示するかだし分けるなどが考えられます。なお、後述する applicationDidBecomeActive のタイミングを拾えてしまい、同一の画面で2度実行されてしまう可能性あるため、そのような場合にはこのライフサイクルでの判定は不要になります。

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
        showForceAlertIfNeeded()
    }
applicationDidBecomeActive

アプリがタスクキルされていない場合も考慮して、このライフサイクルでの判定も行います。本メソッドをフォローすることでアプリのbackground <-> forgroundの遷移でも判定を行うことができます。具体的な実装はアプリのメイン画面(例えばRootViewController)にNotification経由でライフサイクルを注入するのが良いと思います。

    func setup() {
        NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
    }
    
    @objc func applicationDidBecomeActive() {
        showForceAlertIfNeeded()
    }

合致するビジネス要件

RemoteConfig を使用した強制ダイアログ機構は大変便利なものではありますが、RemoteConfigの特性上ビジネス要件次第では採用が難しいパターンがあります。それはダイアログの表示を判断してから実際に表示されるまでのタイムラグを可能なかぎり0にしたい場合です。

Firebase RemoteConfigはその機能の仕様上、具体的には1時間あたりのフェッチ回数の上限があるという仕様上、前回の値所得時から新規の値取得時まで一定の期間設定値の再取得を行わないようになるパターンが多いです。したがって、このような場合にはダイアログに関するFirebase側の設定変更から実際のアプリ側への反映まで一定のタイムラグが発生してしまいます。

よって、設定変更から反映まで即座におこないたいという厳し目の要件の場合は合致しづらいですが、多少のタイムラグを許容できる場合は本記事の機構で十分実用にたりうると判断しています。

まとめ

強制ダイアログ表示はサービスメンテナンス時やなんらかの不具合によってアプリの全体的なバージョンアップが必要な時などに必要となり、実装しておいた方が良い機能かと思います。サーバーの初期通信などに強制ダイアログ表示機構の返却値等を入れた場合、サーバーがそもそも動作していない場合などにはダイアログ表示に必要な情報を取得することができません。

強制ダイアログ表示を行うための機構をサービスサーバーから分けることで、サーバーが機能していない場合でもメンテナンス中などの表示を行うことができます。機能要件の中で出てきた際には是非選択肢の一つとして検討してみてください。

参考資料

RemoteConfig

AdvcentCalendar

サンプル

【Swift】Swift5.1からOptionalのEnumのswitch文をより簡単にかけるようになった

はじめに

Swift5.0まで OptionalEnumインスタンスに対してswitch文を使用する際は下記のように.some(T).noneで記述する必要がありました。

enum Frequency {
    case daily
    case weekly
    case monthly
    case yearly
}

let frequency: Frequency? = .daily
switch frequency {
case .some(.daily): print("daily")
case .some(.weekly): print("weekly")
case .some(.monthly): print("monthly")
case .some(.yearly): print("yearly")
case .none: print("nil")
}

これは Swift では Optional が下記のような enumで表現されていることに起因します。

enum Optional<T> {
    case some(T)
    case none
}

このswitch文での分岐に関してSwift5.1で変更があったため記載します。

環境設定

以下の環境を使用しています。

  • Swift5.1

内容

Swift 5.1から Optionalenumのプロパティをswitch文で分岐させる際に、 .some(T) のように記載する必要がなくなりました。 具体的には「はじめに」に記載したコードをSwift5.1では下記のように記述することができます。

enum Frequency {
    case daily
    case weekly
    case monthly
    case yearly
}

let frequency: Frequency? = .daily
switch frequency {
case .daily: print("daily")
case .weekly: print("weekly")
case .monthly: print("monthly")
case .yearly: print("yearly")
case .none: print("nil")
}

まとめ

細かな変更ですが .someの記載が不要になったことで . を打つだけで分岐先の候補が一覧で補完されるようになり、書き味がより良くなったと思います。