CircleCIとDangerでPull Requestの自動チェックをやってみた

これはなに

Dangerとは

github.com

  • DangerはPull Requestの状態をチェックするツール
  • マイルストーンアサインが未設定のとき、マージ先が特定のブランチに向いてないとき、コメントで警告したりPRを失敗させたりできる

Dangerのインストール

  • DangerはRuby製でGemからインストールできる
  • Dangerには複数のプラグインがあり、今回はSwiftLintをDangerで実行させる danger-swiftlint を導入した
gem "fastlane", "2.94.0"
gem "danger", "5.5.13"
gem "danger-swiftlint", "0.16.0"

Dangerの初期化

  • bundle exec danger init するとDangerfileができる
  • DangerfileにはPull Requestでチェックして欲しい状態や条件を記述する
  • 俺はブランチのチェックは警告にしたが、失敗にすると安心で安全な開発環境をつくれると思う
# RPの差分範囲外に対する指摘はすべて無視
github.dismiss_out_of_range_messages

# --------------------
# swiftlint
# --------------------
swiftlint.config_file = '.swiftlint.yml'
swiftlint.lint_files inline_mode: true

# --------------------
# pr title
# --------------------
warn('このPRは作業中です') if github.pr_title.include?("WIP") || github.pr_title.include?("wip")

# --------------------
# base branch
# --------------------
is_to_master = github.branch_for_base == 'master'
is_to_release = github.branch_for_base.include?("release")
is_from_release = github.branch_for_head.include?("release")
is_from_development = github.branch_for_head.include?("dev") || github.branch_for_head.include?("development")

warn('master へマージできるのは release branch のみ(緊急時はOK)') if is_to_master && !is_from_release
warn('release へマージできるのは dev branch のみ(緊急時はOK)') if is_to_release && !is_from_development

# --------------------
# milestone
# --------------------
warn('このPRにマイルストーンを設定してください') if github.pr_json["milestone"].nil?

# --------------------
# assignee
# --------------------
warn('このPRにアサインしてください') if github.pr_json["assignee"].nil?

Dangerの実行

  • 実行方法は色々あるが、今回はfastlaneから実行させた
  • 個人的にツールの実行や環境変数など可能な限りFastfileに記述して、CIはfastlaneの実行とmacOSのセットアップに留めたい派です
danger(
  danger_id: "unit-tests",
  dangerfile: "tests/MyOtherDangerFile",
  github_api_token: ENV["GITHUB_API_TOKEN"],
  verbose: true
)

https://docs.fastlane.tools/actions/danger/#danger

Dangerを導入した目的

  • 目的は新メンバーへのオリエンテーションを少しでも良い感じにすることだった
  • オリエンテーションを「あーでこーで」みたいなレクチャー形式にするのではなく、開発中に気がつける仕組みにしたかった
  • コーディングしてコミットしてPR投げたときに、Dangerの警告で気づける方がスマートだ
  • 必要な情報が知るべきタイミングで共有される仕組みはとても良さそう
  • 実際のDangerfileには警告にesawikiへのリンクを追記してる

CircleCIからDangerを使うとき困ったこと

  • PRをGitHubから新規作成したときにDangerの実行がスキップされる
  • PRを作成した後にコミットをpushするとDangerは実行される
  • CircleCIがPR作成時に該当のPRのURLを環境変数に設定してないからスキップされるようだ
  • CircleCIの中の人曰く、今のところはPRを作った後にリビルドして対応してくれとのこと 😢
    • 解決方法は先頭に書いた!

For now, rebuilding is the only suggestion I have while our engineers look into this further.

https://discuss.circleci.com/t/circle-pull-request-not-being-set/14409/11discuss.circleci.com

https://discuss.circleci.com/t/trigger-new-build-on-pr/4219discuss.circleci.com

なぜCircleCIを選択したのか

  • 会社のCIツールを統一したくてCircleCIを選択した
  • しかし、CircleCIにこのような不便さがあるとは知らなかった
  • 個人開発では Bitrise を使っててPRの作成をトリガーに出来たので Bitrise に移行しようかなと心が揺れている 解決できた今はあまり揺れてない
  • せっかくCIツールを統一できて気持ち良い感じだったのに残念
  • 何か良い対策を知ってる人は教えてください 🙇‍♂️ 教えてくれました!

まとめ

  • CIからDangerを使うとPRの状態を自動チェックする仕組みがつくれる!!
  • 必要な情報が知るべきタイミングで共有される仕組みがつくれる!!
  • CircleCIから使うときは、Only Build pull requests を有効にしてPR作成時のスキップを回避しよう!!

副業の始め方と副業で得た知見

これはなに

  • 副業を初めて一ヶ月が経過した
  • 副業で始めて給料を得た
  • 開始してまだ一ヶ月しかたってないがそこで得た学びを残す

どんな副業なのか

具体的には言えないのでざっくりと説明したい

  • iOSの開発支援で新機能の開発やバグの修正などが主なタスク
  • 自宅からリモートで平日の週3で2~3時間ほど稼働してる
  • 給料は時給制で値段は言えない
  • 高時給で儲かるような契約ではないが、そこには納得して副業している

なぜ副業を始めたのか

ここからが本題で理由は2つある。

① 本業は辞めたくないけど、本業とは別のサービスを開発したかった

転職はしたくないけど別のことはしたい。そう考える人は多いと思う。 大きな会社だと部門の移動とかで解消できる悩みかもしれない。 でも、今の本業だとそれは不可能なので副業という選択を取るしかなかった。

② リモートで開発を経験してみたかった

リモートで開発したいのは自分が将来、東京以外の場所で働く可能性があるから。 地元が田舎で親の体調によっては将来、実家に戻らざるをえない状況になるかもしれない。 そのリスクが問題になる前に対処できるスキルを持ってないと心配だなと思った。

でも、本業でリモート開発するには色々と越えないといけないハードルが多い。 そこで副業で出来ないかなと思ってたら幸運にもリモートで副業ができるようになった。

どうやって副業を手にしたのか

少し前に connpass で副業に関するイベントがあって参加した。

engineer-parallel-work.connpass.com

このイベントでは副業中のエンジニアが副業に関する知見をLTで発表してた。 内容をまとめると以下のような感じだった。

  • 副業を始めるには周囲に副業したいことをアピールし続けること
  • 副業未経験の人は受託案件はハードルが高いので最初はやめた方が良い
  • 副業未経験の人はスタートアップで開発支援などで参加するのがベター
  • 副業未経験の若者は、副業で儲けようとするのではなく、スキルアップしながらお小遣いが稼げるぐらいの気持ちで始めると良い。後にその経験が大きなリターンで返ってくる。
  • 発表してた人たちは全て知人からの紹介で副業を得ていた
  • 逆にWebサービス等で副業を得た人はいなかった

Webサービス等で副業を開始した人がいなかったのは、この市場が発展途上だからかもしれない。 またイベントのLTで発表できるような人たちなので、そこもバイアスがかかっているだろう。

でも、副業をお願いしたい会社側も見ず知らずのエンジニアに頼むよりは、知人経由での紹介の方が安心なのでやっぱり知人経由だと思う。 そして、自分も知人経由で副業を開始できたので間違いではないと思う。

特に、「周囲に副業したいことをアピールし続けること」 は一番大事というか、これをしないと始まらないので副業したい人は行動しよう。

今の副業で得た知見

最初は副業のメリットとデメリットで書こうと思ったけど、明確にメリットとデメリットに分けられないのでやめた。 メリットとデメリットが混在するというか、その狭間で右往左往するような感じ。 要は、読み手の立場によってどちらにも取れるので知見という言葉で濁すことにした。

知らない慣れてない技術に向かう機会が増える

当たり前すぎるが、これが面白いから副業してる。

本業は自分が書いたコードが多いし環境にも慣れている。 すると、慣れたことでやり続けてしまって成長の機会が減る。

自力で自分の殻を破って成長できる人は素晴らしいが、僕は環境を変えて半強制的に殻を破らざるをえない場所に身を置く方を選びたかった。 それなら転職すればいいのではと思うかもしれないが、そこまでの気持ちが今はないのは既に書いた。

あと、本業と副業で別々の仕事をすると、お互いを比較することで色んな改善策がポツポツと頭に浮かんでくる。 何かと何かを比較してそこに生まれる差分でいろんな気づきを得られるのも大きいと感じている。

もちろん、GitHub上のOSSを見ることで似たような体験を得ることは十分可能なんですが。

確定申告と請求書とかの知識がつく

副業を始めるのが面倒だと思う理由は、確定申告とか請求書とかがイマイチよくわからないからだと思う。 自分もここが心配だったが、知人の助けにより今のところは問題なさそうである。

要するに、ネットバンクで口座を開設してfreeeで開業届けを出して請求書もfreeeで作成するでOKだった。 まだ確定申告という難関を突破したことはないが、とにかく本業以外の収入を計算しやすいようにひとまとめにしておくことが大切で、あとは詳しい人に聞けばなんとかなる。

今までfreeeを使ったことなかったけど、色々便利ですごいなと思ったし、開業届けや確定申告などを自分事として考えられるようになったのは嬉しい。 学ぶ喜びを得ている。

リモートで改めて気づく報連相の大切さ

いつから稼働して、何時に完了して、何をやったのかをわかりやすく伝えないと、副業先の会社の人は心配になる。 当たり前だけど大切なことである。

稼働の開始と終了はslackでやり、開始時に今日の作業内容を簡単に書く。終了時には作業の進捗を書く。 作業の進捗や結果がわかりやすいように、WIPつけたりPRにはスクショなども貼る。

大きめの修正や仕様に大きな変更がありそうなとき、相手側の要望とその対応をまとめてissueにする。 正確にissueにまとめられてると、リモートでもコミュニケーションは比較的円滑にすすむ。 自分以外の人も副業でリモートしてるので、他のメンバーもお互いの作業が把握しやすくなる。

リモートだからとか関係なく、普通にやるべき事だけどその大切さを改めて実感してる。

ささいなことで褒められる

最後は承認欲求が満たされるてやつだ。

副業を必要としている会社は人手が足りてないので、不具合とか色んな問題が放置されがち。 ほんの数分でなおせそうな不具合とかも残ってるので、すぐに対応すると喜んでもらえる。 当たり前すぎるけどやってて楽しい。

まとめ

  • 副業はじめました
  • 副業を始めるには周囲に副業したいことをアピールし続けることが何よりも大切です

余談

今年のiOSDCの開催が発表されたので、発表する内容を準備しないと・・・。

http://blog.iosdc.jp/entry/2018/04/23/100000blog.iosdc.jp

potatotips 49 で発表してきた

これはなに

potatotips.connpass.com

なにを発表したの

speakerdeck.com

発表の補足

 @tarunonさんが発表中にツイートしてくれた。5分なので説明を端折ったが、確かに聞いてた人は疑問に思うところだった。@tarunonさんに感謝。

 なお、今回の発表のサンプルアプリは複数のレイアウトパターンを確認したかったのでサンプルではあるけど、結構いろいろ作り込んでる。iOSのレイアウトをコードで書く人、storyboardの人、両方からのツッコミを期待してる。

 また、実はこのテーマにする前の構想では「コードでレイアウトする際の利点・欠点」について発表する予定だった。でも、先日の TRY! SWIFT AFTER TALKS の1日目で @SatoshiN21さんが言いたいことの9割を言ってくれたのでお蔵入りにした。

 なぜ、9割かと言うとレイアウトはライブラリを使わず、標準のNSLayoutAnchorで書く方が今後の開発にはプラスになると個人的に思っているから。

 CartographySnapKitTinyConstraints など優れたAutoLayoutのDSLがあることは知ってる。でも、AutoLayoutそのものの学習コストがそこまで低くないにも関わらず、DSLになると新メンバーの参入障壁になると思った。もちろん、慣れの問題だし短く書けることに越したことはない。

 それ以外は同じような理由でコードでレイアウトを書いてる。

potatotipsで発表して思ったこと

 potatotipsは発表時間が5分と短い。だから、自己紹介の後すぐに結論をまとめたスライドを出した。これは正解だったと思う。でも、早口で噛んだりちょっと時間オーバーしたのが悔しい。potatotipsでは、これまでに有名なiOSエンジニアがたくさん発表してるので結構プレッシャーだった。

 優れた発表者は総じて明瞭で簡潔なスライドできっちり時間内に説明してた。今回のpotatotipsでは@ra1028fe5さんの発表が良かった。*1 *2

 自分の発表は勉強会のハッシュタグを追っていると、勉強になるとツイートしてくれた人がいたので安心した。前回の発表よりも反応があったので嬉しい。一歩前進した。

 でも相変わらず、資料づくりは大変だ。前回に比べて発表時間が半分の5分で、しかもスライドのテーマとか細いところは前回の発表で固まったので内容だけ考えればよいはずだった。それでもサンプルアプリの作成、公式ドキュメントの下調べ、社内のiOSエンジニアにレビューして欲しいので発表前日には完成させる等、やること多い。やって無駄なことは一つもないけど、5分の裏側はいろいろ大変だった。発表者の全員がそうだと思う。

 発表資料の作成もAutoLayoutと同じで慣れの問題だろうか・・・。

終わり

  • 2018年にやりたいことの二つ目を達成した
  • iOSDCまでにより良い知見を発表できるよう精進する

*1:発表内容とは関係ないけど、UICollectionViewにorderableのAPIがなかった時代に、RAReorderableLayout を参考にして実装したのを発表後に思い出した。

*2:自分がiOSエンジニアなのでiOS側の発表にバイアスがかかってるけどAndroidの発表も凄かった

第2回 iOS UI実装勉強会で発表してきた

これはなに

  • 下記の勉強会で発表してきた
  • 人生初の社外勉強会での発表だった
  • Speaker Deck にいくつか資料はあげてるけど、他のは全て社内で発表した資料だった

connpass.com

なにを発表したの

speakerdeck.com

発表して思ったこと

 発表てリスキーだなと思う。良い発表ができれば人の役に立ち有名になるだろうけど、間違った情報を発表をすると悪いイメージを与えてしまう。しかも、その場での訂正が難しいのでブログやQiitaよりも影響が大きい。社外だと自分のバックグラウンドを知らない人ばかりなので、共有していない情報が多過ぎてあらぬ誤解を与えるかもしれない。

 だから可能な限り正しい情報を発表するため、資料作成には非常に時間をかける。何度もドキュメントを見て、本当に正しいのかテストを繰り返す。これだとブログやQiitaで書いた方が、間違ったとしても訂正して後で知らせれるかもしれないし、コメントや編集リクエストも来るかもしれない。時間や場所に縛られない。発表で緊張して精神をすり減らす必要もない。なぜ、みんなは知らない人の前で発表したがるのだろうか。

 大げさに書いたけど、そんなことを自分はどう感じるのか確かめたくて、実際に社外で発表してみた。結果は可もなく不可もなく。達成感はあった。今回発表した内容についてさらに詳しくなった。今回の発表内容がどれぐらい役に立ったのかわからないので成果はそれだけだ。それだけと言うと、言い過ぎな気もするが。もしかしたら、自分が気づいていない成功や失敗があったのかもしれない。

 発表のハードルを上げ過ぎている!少しぐらい間違ってもいいのでは?という意見もあると思う。でも、間違った情報を流すことは避けたいし、少しでも役に立つ内容にしたい。そんなモヤモヤを糧にして技術力を上げていく方法をとるのもいいのかもしれない。既にどこかの誰かが発表駆動学習とか言ってそうだ。

まとめ

  • 2018年にやりたいと思ってたことを一つ達成できた
  • 発表駆動学習をもう少し実践してみたい
  • 今年のtry!Swiftには無理だったけど、iOSDCでは何か発表したいと密かに思ってる

テスト駆動開発の第一部をSwiftで写経してみた

これはなに?

  • 社内でテスト駆動開発(Test Driven Development = TDD)の勉強会を行った
  • これはそのときの発表資料です
  • 自分はiOSエンジニアなのでSwiftでやってみた
  • 参加した同僚達は Go, Ruby, Dart, Elm などで写経している
  • 第一部を写経する前に「付録C 訳者解説:テスト駆動開発の現在」を先に読むのをオススメする
  • 社内勉強会では、付録Cの概要をみんなで共有してから第一部の写経に進んだ

Swiftで写経

  • Swiftで出来るだけ書籍通りに書いてみた
  • 出来るだけ同じように書かないと写経できなくなるので、Swiftでは非推奨な書き方もある
    • そこは後述で補足する
  • コミットは書籍でテストを走らせたタイミングや、ある程度意味のありそうな塊できった
    • 電子書籍だからページ番号がフォントサイズで変わるので、どの章のコミットかわかるようにメッセージの先頭に「第n章」とつけた
  • TDDの説明ではなく、JavaとSwiftの言語仕様の違いが際立ってしまった
    • 写経することがTDDを理解することの近道なので写経がまだの人は写経してください
    • TDDの背景や文脈は付録Cに書かれている

第1部の内容

  • 2つの仕様を満たすプログラムをTDDで開発していく
// ① 金額に数値を掛けて金額を算出する
$5 * 2 = $10
// ② 異なる2つの通貨を足し、為替レートに基づいて換算された金額を算出する
$5 + 10CHF = $10 (レートが USD:CHF = 2:1 の場合)  
  • 書籍では米ドル(USD)と瑞西フラン( CHF)を例にしている
  • 書籍ではJavaで書かれている
  • 第1部は全部で17章
  • 最後の17章はまとめなので、実際は16章まで

第1〜16章のコミットを追いながら説明

  • git log --oneline --reverse --pretty=format:"%s" でコミットメッセージを章ごとに表示
  • 書籍どおり、章の終了時点のコードがわかるようにGitHubのリンクを貼っておく
  • JavaとSwiftの言語仕様の違いとか、TDDの特徴で気になった点に注目していく

第1章

第1章 MoneyTestを作成
第1章 Dollarクラスを作成
第1章 Dollarクラスにイニシャライザ作成
第1章 Dollarクラスにtimesメソッド作成
第1章 Dollarクラスにamountプロパティを作成
第1章 テストを通すためにamountの初期値を10にする
第1章 重複の削除のためにamountの値を具体的にする
第1章 Dollarのイニシャライザでamountプロパティの初期値を設定する
第1章 timesの重複を削除するために5をamountに変更
第1章 timesの2のベタがきをmultiplierに変更
第1章 *=演算子をつかって重複を削除

第2章

第2章 Dollarの副作用でオブジェクトの状態が変更されてしまうが理想のテストを書く
第2章 もしもtimesメソッドから新しいオブジェクト返るなら
第2章 timesから新しいDollarオブジェクトを返せるような実装に変更する
第2章 timesからDollarオブジェクトを返す

第3章

第3章 equalsメソッドのテストを作成
第3章 equalsメソッドを作成してとりあえずtrueを返す
第3章 2つ以上の実例を作るために「$5 != $6」のテスト作成

第4章

第4章 Dollar同士を比較するようにしてテストの意図を明確にする
第4章 もう一方も同じようにDollar同士を比較する
第4章 一時変数のproductは不要なので削除してインライン化する
第4章 amountプロパティはprivateにして自分自身しか参照しなくて良くなった
第4章(番外編) Javaのequalsに相当する、Equatableの「==(lhs: Self, rhs: Self) -> Bool」を実装してテストを通す

第5章

第5章 ひとまずDollarのテストをコピーして、Francのテストを作成
第5章 DollarクラスをコピーしてFrancクラスを作成
  • 第5章終了時のソース
  • ①テストを書く→ ②コンパイラを通す→ ③テストを走らせ失敗を確認する → ④テストを通す → ⑤重複を削除する というサイクルで進めたい
  • ①~③を早く終わらせて④に出来るだけ早く着手したいから、DollarのテストとクラスをコピーしてFrancを作る
  • 要は、ただDollarコピペしてFrancにリネームしただけ

第6章

第6章 親クラスとしてMoneyクラスを作成
第6章 親クラスのMoneyにamountプロパティを引き上げる
第6章 Francの等価性のテストを忘れてたので追加する
第6章 FrancクラスもMoneyクラスを継承する
第6章 親のMoneyクラスにあるのでamountプロパティを削除
第6章 equalsメソッドのキャスト型をMoneyに変更する
第6章 Francクラスのequalsメソッドも親クラスのMoneyクラスに引き上げる

第7章

第7章 DollarとFrancは別の通貨であるというテストを追加
第7章 金額だけでなく、オブジェクトの型も同じなら等価とする
  • 第7章終了時のソース
  • 第6章の作業中に生じた疑問「DollarとFrancを比較するとどうなるのか?」をテストしてみる
  • 作業中に生じた疑問をテストにして確認するサイクルを紹介
    • 「あの条件・状態の時、どうなる?→テストコードで実験」という流れ
    • テストがあるとこういうの楽
  • ひとまずテストを通すようにコードを修正

第8章

第8章 timesメソッドの返値をDollarとFrancで同じにする
第8章 DollarのFactory MethodをMoneyに作成する
第8章 Dollarクラスへの参照を減らすために型宣言を親クラスのMoneyに変更する
第8章 DollarのFactory Methodの返り値の型をMoneyに変更する
第8章 Dollarを参照している箇所をFactory Methodに置き換える
第8章 Dollarと同じくFrancもFactory Methodを作成してFrancの参照をなくす

第9章

第9章 通貨(Currency)のテストを追加する
第9章 通貨名をサブクラスに格納する
 - Swiftではクラス内に同名で同型のプロパティとメソッドは定義できないので_を接頭辞につける
第9章 currencyを親クラスのMoneyに引き上げる
第9章 Francのイニシャライザで通貨名を渡せるようにする
第9章 Francのイニシャライザを呼び出している箇所のエラーを解消する
第9章 自身のイニシャライザを呼ばずに、親クラスのFactory Methodを呼ぶ
第9章 FrancのFactory Methodから通貨名を渡すように変更する
第9章 イニシャライザのcurrencyをプロパティに代入する
第9章 Francと同じ変更をDollarにも行う
第9章 サブクラスのイニシャライザを親クラスに引き上げる
  • 第9章終了時のソース
  • DollarとFrancのクラス自体を消し去りたいので、通貨(Currency)の概念を導入したい
    • じゃあ通貨(Currency)をMoneyに持たせて...とするのではなく、Moneyが通貨を持った状態のテストを先に書く
    • 書いたテストが成功するような実装を書いていく
    • 同じサイクルの繰り返し

第10章

第10章 timesメソッドで返すクラスを自身のイニシャライザを使って行う
第10章 ベタ打ちではなく、currency変数に置き換える
第10章 Franc型とMoney型の区別が必要なのか判断するためにtimesでMoneyを生成して返すように変更してみる
第10章 Moneyのtimesを抽象メソッドではなく具象メソッドに変更する
 - Swiftの場合、XCTAssertEqual failed: ("Optional(TDDForSwift.Franc)") is not equal to ("Optional(TDDForSwift.Money)") のエラーになる
第10章 エラーメッセージを少しマシなものにするためにデバック出力を編集する
 - SwiftはCustomStringConvertibleのdescriptionで、JavaのtoStringと同じ処理を実現できる
第10章 エラーになる前の状態に戻す
第10章 FrancとMoneyが等価であることをテストする
第10章 equalsメソッドでクラスではなく、通貨名を判定するように変更する
第10章 再びFrancのtimesメソッドでMoneyを返すように変更してテストを通す
第10章 Francと同じようにDollarもtimesでMoneyを生成して返すように変更する
第10章 timesメソッドを親クラスのMoneyに引き上げる

第11章

第11章 Factory Methodでサブクラスを初期化する必要はなくなったのでMoneyで初期化する
第11章 Dollarクラスを参照するコードはないので削除できる
第11章 等価性比較の過剰なテストは削除する
第11章 Francクラスの動作を確認するだけのテストとなったため、Francクラスと同時に削除する
第11章 掛け算の振る舞いをクラスごとにテストする必要はないので、不要な方の掛け算の振る舞いテストを削除する
  • 第11章終了時のソース
  • 子クラスを削除する準備が整った
  • 子クラスを削除して、過剰なテストを削除する(重複の削除)

第12章

第12章 足し算のテストを追加
第12章 plusメソッドを実装する
第12章 為替レートをもとに銀行が通過を換算(reduced)するようなテストを作成する
第12章 銀行を初期化する
第12章 2つの和を表現するExpression(式)を作成する
第12章 5ドルを作成して、$5 + $5 のテストを完成させる
第12章 Expressionを作成する
 - SwiftはprotocolでJavaのinterfaceを表現する
第12章 plusメソッドはExpressionを返すように変更する
第12章 MoneyをExpressionに準拠させる
第12章 Bankクラスを作成する
第12章 空のreduceメソッドをBankに実装する
第12章 ひとまずテストを通すためにreduceで10ドルを返す
  • 第12章終了時のソース
  • 掛け算はできたので、次は足し算の実装をスタート
  • やることは掛け算のときと同じサイクル
  • 為替レートの概念が複雑で熟考を要する
  • 12章から複雑になる
    • コミットの数が細かく章ごとのコミット数が多くなっていく
    • Expression(式)を思いつく人と、そうでない人に大きな差があるような気がしてならない(Kent Beck(筆者)も17章で式のメタファーを思いつくまでに多くの時間とプロセスを要したと述べている)
    • つまり、Expression(式)を思いつくまでテスト書いて実験するサイクルを繰り返せてことのようだ
    • 先にExpression(式)が出たけど、DollarができてMoneyができたのなら、SumができてExpressionじゃない?と思った
    • ドルとフランしか表現できなかったときがあったように、足し算しかできないBankができてもこの時点ではよかったのでは?

第13章

第13章 足し算のaugend(被加算数)とaddend(加数)のテストを書く
第13章 Sumクラスを作成する
第13章 plusメソッドでSumを返す
第13章 Sumクラスにイニシャライザを定義する
第13章 SumクラスにExpressionを準拠させる
第13章 Sumのイニシャライザでプロパティに値を代入してテストを通す
第13章 Bankのreduceのメソッドで足し算の結果をテストする
第13章 BankのreduceメソッドでSumのプロパティを足し合わせた値を使ってMoneyを生成して返す
第13章 BankのreduceをSumクラスへ移動する
第13章 BankのreduceメソッドにMoneyを渡したときのテストと、Moneyにキャストする処理をreduceメソッドに追加する
第13章 Moneyにもreduceメソッドを定義する
第13章 MoneyにもSumにもreduceメソッドがあるので、Expressionにreduceを定義する
 - Javaはinterfaceのメソッドはpublicになるが、Swiftはデフォルトのinternal(同モジュール内まで公開)で良い
第13章 Expressionにreduceを引き上げたのでキャストや型チャックが不要になったので削除する

第14章

第14章 2フランを1ドルに換算するテストを書く
第14章 ビルド通すためにBankにaddRateメソッドを空で実装する
第14章 フランをドルに換算するテストを通すために、Moneyのreduceメソッドでレートを計算する
第14章 為替レートをBankで管理できるようにreduceの引数にBankを追加する
第14章 Bankで為替レートの計算を行うrateメソッドを定義する
第14章 為替レートの計算をBankに任せる
第14章 配列の中身を等価だと扱ってくれるのかテストする
 - Swiftは等価だと判断するが、Javaは等価だと判断しない
第14章 先ほどのテストは削除して、Pairクラスを作成する
第14章 Pairクラスをハッシュのキーとして使うために必要なメソッド定義する
 - SwiftはHashableに準拠すれば辞書型(ハッシュ)のキーに使用できるクラスになる
第14章 Bankで為替レートを格納するローカル変数を定義する
第14章 為替レート設定時にレートを格納する
第14章 為替レートを検索されたときに対象のレートを返す
 - Swiftの辞書型(ハッシュ)から値を取り出すとき、値は必ずオプショナル(?)なので暗黙的アンラップ(!)する
第14章 同じ通貨を換算した場合にエラーになるので、同じ通貨を為替レートから検索する場合のテストを追加する
第14章 同じ通貨の場合は為替レートは等倍なので1を返す

第15章

第15章 異なる通貨を足し算して、どちらかの通貨に換算して結果を出すテストを書く
第15章 ひとまずビルドを通すために、Money型で宣言するがテストは通らない
第15章 Sumクラスで被加算数(augend)と加数(addend)を換算してテストを通す
第15章 被加算数と加数の型をExpressionに変更する
第15章 さらにイニシャライザの引数の型もExpressionに変更する
第15章 Moneyのplusメソッドの引数もExpressionに変更できる
第15章 Moneyのtimesメソッドの返り値の型もExpressionに変更できる
第15章 テストケースもMoneyのplusメソッドに渡す型をExpressionに変更する
第15章 fiveBucksの型もExpressionに変更する
第15章 Expressionにplusメソッド引き上げる
第15章 Expressionに準拠しているSumにもplusメソッドを定義する
 - 書籍どおりnilを返したいが、オプショナル型になると別メソッドと認識されるので、addedをとりあえず返す
第15章(番外編) 強制キャストしてMoneyにしないとテストが通らない
 - Swiftの==演算子はEquatableに準拠する型しか比較できず、Expressionはプロトコルなためできない

第16章

第16章 Sumクラスのplusメソッドのテストを書く
第16章 SumクラスのplusメソッドからSumを生成して返す
第16章 Sumクラスのtimesメソッドのテストを書く
第16章 SumクラスのtimesメソッドもSumを生成して返す
第16章 被加算数と加数はExpression型にしたので、Expressionにもtimesメソッドを引き上げる

第17章

  • 1〜16章までTDDのサイクルを以下のように表現してきた
    • ① 小さいテストを追加する
    • ② すべてのテストを動かし、失敗があることを確認する
    • ③ 変更を行う
    • ④ 再びすべてのテストを動かし、すべて成功することを確認する
    • リファクタリングを行い重複を除去する
  • TDDを教えるたびに著者は3つのことに驚く
    • テストを綺麗に機能させる3つのアプローチ。仮実装、三角測量、明白な実装
    • テストとコードの間の重複除去が設計を駆動する
    • テストの間のギャップを制御する能力。路面が滑りやすいならグリップを増し、路面が良いならより速く

まとめ

  • 第1部の総コミット数は122でした
  • 写経するのが一番理解が早いと実感した
  • Xcodeで全てが完結するのでTDDの写経は楽だった
  • TDDの問題ではないがテストが欲しい大きなプロジェクトほどiOSではビルドに長い時間がかかるのでスムーズにTDDができず歯がゆい

GithubKitForSampleを参考にしてQiitaKitForSampleを作ってみた

これはなに

この記事はiOS2 Advent Calendar 2017の5日目の記事です。

今年のiOSDCで注目を集めた iOSDesignPatternSamples を参考にしてiOSデザインパターンを勉強してる。このiOSDesignPatternSamplesは、GithubKit というライブラリを使って開発されている。

今回は、このGithubKitが面白かったので自分も真似して作ってみたという話。しかし、ただ真似するだけではコピペになるし学びが薄いので、APIをQiitaに変更して開発してみた。

github.com

github.com

先に作った感想

結論から先に言うと、とても勉強になったし、@marty-suzukiさんには感謝しかない。

具体的に良かったのは、「SwiftやiOSの新機能をさっと試すのに適当なサイズのサンプルアプリが欲しいとき、◯◯Kitというライブラリをフレームワーク化しておくとサンプルアプリを作りやすい」 ことに気づけたところ。

今までは個人で開発したアプリで新機能を試してたが、一度リリースしてしまうと既存ユーザーへの影響やデザインパターンとの調整を考えて、新機能を試すのが面倒になり何もしないことが多かった。

かといって、ゼロからサンプル用のアプリを作るのも面倒で、サンプルに適した形にするのもまた面倒だ。

結局、なにも試してない状態になってた。要するに、サボってた。

でも、この〇〇Kitとかを作るとcarthageでプロジェクトに取り込んでささっと適当なサイズのアプリにして新機能を試せていいかなと思い始めてる。

もちろん、試したい機能によって下準備とか別に必要だけど、それもフレームワーク化して 「新機能をすぐに試せる環境作りをしておくのが大切なんだ」 と実感した。

QiitaKitForSampleについて

github.com

本題に入る。GithubKitはSwift3の時代に開発されてたようで、Swift4の機能は使われていない。

というわけで、QiitaKitForSampleではSwift4の新機能の Codable を使ってAPIのリクエスト、レスポンス、モデルを書き換えてみた。

リクエスト(Requestable)

まずは、リクエストの部分。ResponseTypeをCodableにしてリクエストして返ってきたデータをレスポンスにデコードする static func decode(with data: Data, response: HTTPURLResponse?) throws -> Response<ResponseType> を定義した。

public protocol Requestable {
    associatedtype ResponseType: Codable

    static var baseURL: URL { get }
    static var notFoundText: String { get }
    
    var allHTTPHeaderFields: [String: String]? { get }
    var endpoint: URL { get }
    var path: String { get }
    var method: HttpMethod { get }
    var body: [String: Any] { get }
    
    static func decode(with data: Data, response: HTTPURLResponse?) throws -> Response<ResponseType>
}

レスポンス(Response<T: Codable>)

次にレスポンスではリクエストの static func decode(with data: Data, response: HTTPURLResponse?) throws -> Response<ResponseType> で処理するレスポンスのデコードメソッドを定義した。

singleはその名のとおりネストのないJSON(ex: {})の場合、unkeyedContainerは配列(ex: {},{})の場合に使う感じを想定して定義した。

ちなみに、.formatted(DateFormatter.ISO8601).iso8601 でもOKだが、iOS9~11の対応を想定して開発したので独自で定義したDateFormatterを呼んで渡してる。

public struct Response<T: Codable> {
    public let totalCount: Int
    public let values: [T]

    init(single data: Data, response: HTTPURLResponse?) throws {
        let strTotalCount: String = response?.allHeaderFields["Total-Count"] as? String ?? ""
        self.totalCount = Int(strTotalCount) ?? 0
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .formatted(DateFormatter.ISO8601)
        self.values = [try decoder.decode(T.self, from: data)]
    }
    
    init(unkeyedContainer data: Data, response: HTTPURLResponse?) throws {
        let strTotalCount: String = response?.allHeaderFields["Total-Count"] as? String ?? ""
        self.totalCount = Int(strTotalCount) ?? 0
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .formatted(DateFormatter.ISO8601)
        self.values = try decoder.decode([T].self, from: data)
    }    
}

モデル(User: Codable)

そして、レスポンスとなるモデルはスネークケースのキー名のみ、独自で定義すればOKなので簡潔になった。

public struct User: Codable {
    public let id: String
    public let name: String?
    public let profileImageUrl: URL
    public let followeesCount: Int
    public let followersCount: Int
    public let itemsCount: Int
    public let websiteUrl: String?
    public let location: String?
    public let organization: String?
    public let description: String?

    private enum CodingKeys: String, CodingKey {
        case id
        case name
        case profileImageUrl = "profile_image_url"
        case followeesCount = "followees_count"
        case followersCount = "followers_count"
        case itemsCount = "items_count"
        case websiteUrl = "website_url"
        case location
        case organization
        case description
    }
}

GET /api/v2/users/:user_id/followees

最後に、リクエスプロトコル(Requestable)とレスポンス(Response<T: Codable>)とモデル(User: Codable)を組み合わせて、フォローしているユーザーの一覧を返すリクエストは下記のようになった。

このUserFolloweeRequestを見るだけで、どのようなリクエストでレスポンスとして何が取得できるのか、はっきりするのが良いですね。

public struct UserFolloweeRequest: Requestable {
    
    public typealias ResponseType = User
    
    public static let notFoundText: String = "フォロー中のユーザーがいません"
    public let endpoint: URL
    public let path: String = "users/%@/followees"
    public let method: HttpMethod = .get
    public let body: [String: Any] = [:]
    
    public init(page: Int, perPage: Int, userId: String) {
        let basePathURL: URL = UserFolloweeRequest.baseURL.appendingPathComponent(String(format: path, userId))
        var components: URLComponents? = URLComponents(url: basePathURL, resolvingAgainstBaseURL: true)
        components?.queryItems = [URLQueryItem(name: "page", value: String(page)),
                                  URLQueryItem(name: "per_page", value: String(perPage))]
        self.endpoint = components?.url ?? basePathURL
    }
    
    // MARK: - ResponseType decode
    
    public static func decode(with data: Data, response: HTTPURLResponse?) throws -> Response<User> {
        return try .init(unkeyedContainer: data, response: response)
    }
}

Codableを使うことで、Swift3のときにResponseTypeに準拠させていたJsonをdecodeするためのプロトコル(JsonDecodableやJsonDecodeError)は不要となり、Codableが全てを処理してくれるようになった。

Codableへの移行が簡単だったのは、GithubKitの設計が良かったからだと思う。

終わりに

まだまだ発見がたくさんあったけども、それはまた別の機会にする。

当初の目的は、iOSDesignPatternSamplesでiOSデザインパターンを学ぶことだったので。

本家のiOSDesignPatternSamplesはGitHubKitなので、QiitaKitをつかってiOSDesignPatternSamplesの理解を深めていきたい。

ただコードを眺めたり、コピペするだけではやっぱり理解できないですね。

しかし、年内に終えるつもりだったのに間に合わない気がしてたぞ!!!!!!

xibとコードの両方から利用できるカスタムビューを作る

xibのイニシャライザと、コードのイニシャライザ

引き続き、下記のアプリで行ったことをアウトプット。

ボルダー

ボルダー

  • masashi sutou
  • スポーツ
  • 無料

xibのイニシャライザは init?(coder aDecoder: NSCoder) で、コードのイニシャライザは init(frame: CGRect) または独自で定義したイニシャライザになる。

上記のアプリを作成したとき、xibとコードの両方から利用できるカスタムViewが欲しくなった。

具体的に説明すると、Cellで画像を表示するViewはCellがStoryboardで定義されているのでxib経由で初期化したい。 一方、上記のアプリは画像や動画を複数同時にアップロード可能で画像Viewの数が一定ではないため、コードで初期化をコントロールしたい。

つまり、UIImageViewを継承したカスタムなイメージビューをxibとコードの両方から呼べるようにして、一つのカスタムビューで対応したいということ。

カスタムビューのイニシャライザ

MediaViewという名前のカスタムビューを作成して対応した。 xibとコードの両方から利用できるように下記のようなイニシャライザにした。 コードはSwift3です。

import UIKit

final class MediaView: UIImageView {

    private(set) var url: URL?
    private let indicator: UIActivityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
    private let playImageView: UIImageView = UIImageView(image: UIImage(named: "play_icon"))

    init() {
        super.init(frame: .zero)
        isUserInteractionEnabled = true
        contentMode = .scaleAspectFit
        clipsToBounds = true
        translatesAutoresizingMaskIntoConstraints = false
        initialize()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initialize()
    }
    
    private func initialize() {
        
        indicator.hidesWhenStopped = true
        indicator.isHidden = true
        indicator.translatesAutoresizingMaskIntoConstraints = false
        addSubview(indicator)
        
        playImageView.contentMode = .scaleAspectFit
        playImageView.isHidden = true
        playImageView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(playImageView)
        
        NSLayoutConstraint.activate([
            indicator.centerXAnchor.constraint(equalTo: centerXAnchor),
            indicator.centerYAnchor.constraint(equalTo: centerYAnchor),
            indicator.widthAnchor.constraint(equalToConstant: indicator.frame.width),
            indicator.heightAnchor.constraint(equalToConstant: indicator.frame.height),
            playImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
            playImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
            playImageView.widthAnchor.constraint(equalToConstant: playImageView.frame.width),
            playImageView.heightAnchor.constraint(equalToConstant: playImageView.frame.height)
        ])
    }
    
// 省略

xibとコードの両方で利用されるものをinitialize()というメソッドにまとめた。

コードで利用する場合はframeではなく、AutoLayoutでレイアウトを定義したかったのでtranslatesAutoresizingMaskIntoConstraints = falseにした。

今回はアプリの仕様上、初期化後にレイアウトが不変だったのでinit()で済ましたが、override init(frame: CGRect)にしてframeを渡しても良い。

まとめ

  • xibのイニシャライザは init?(coder aDecoder: NSCoder) で、コードのイニシャライザは init(frame: CGRect) または独自で定義したイニシャライザになる。
  • 例えば、xibとコードの両方で利用されるものをinitialize()というメソッドにまとめて、両方のイニシャライザから呼べば良い。