kicksterter/ios-oss を観察してみて思ったこと(その1)
海外のクラウドファンディングサイト kicksterter.com は自社の iOSアプリ のソースコードをGithubで公開しています。
なかなか評判が良さそうだったのでソースコードを観察してみました。
最も印象的だった部分はだいたい他の人の記事で分かりやすくまとめられていたので、 僕は俯瞰的な視点で思ったことをダイジェスト的に書くことにしました。
動機
僕は現在新規のiOSアプリプロジェクトに参加しています。 アプリ開発は最初が肝心なので、なるべく他のプロジェクトを参考にして構成を参考にしようと思い、オープンソースで公開されているkicksterterのiOSプロジェクトを観察してみました。
アプリ開発は最初が肝心
ソフトウェア開発も最初が肝心です。 初期段階のフェーズでの判断次第で、未来の開発環境が大きく変わります。
アプリ開発でも、最初に作った基盤の上で開発していくので長期間の開発を経るほどにその基盤を変更するコストはどんどん膨らんでいき修正するのが難しくなっていきます。 開発をするほどに技術的負債は膨らんでいってしまいます。
初期段階で急いで開発をして技術的負債を多く残しつつもアプリをリリースし、汚いコードやイケてない仕組みの上で実装を続けていくしかないプロジェクトも数多く存在しているはずです。
途中で技術的負債を解消するために経営陣を説得してサービスの進化を一時停止させてコストを投資し途中からリファクタを頑張ることも可能ですが、
最も良い方法は、もともと最初から技術的負債を最小限に抑えることです。
開発初期の段階で適切な設計や仕組みを導入する決断をしていき、なるべく良い開発環境を整え、なるべく良いレールを敷いておくことが大切です。
そのためにまずはOSSなどでしっかりとした構成で作られたプロジェクトに目を通しておくと大変参考になります。
ロケットを打ち上げてすぐの頃は、少し方向を変えるだけで後に大きな違いとなる
— takuya (@taku_oka) 2017年7月9日
今はまだその初期段階だから、正しい方向に動かせばもっと良くなるはず pic.twitter.com/PRVyPyJsQx
読んでみた結果
README.mdに、アプリのコードをOSSとして公開する理由は “教育のため” と書いてありましたが、まさに勉強になりました。しっかりとしたモダンな構成になっているので、iOSアプリのプロジェクト例としてとても参考になります。 このプロジェクトはだれでも見ることができるので、新規でアプリを作る人は目を通しておけば何かと得るものがあると思います。
前置きが長くなってしまいましたが、
印象的だった点をいくつかピックアップして書いていきます。
どこまでOSSにしてるのか
オープンソースとはいえ、さすがにサーバーサイド周りの機密情報は公開してません。 秘密にすべき情報は Secrets.swift に集約されており、オープンソース上では例としてウソの情報が入っていました。
Secrets.swift の isOSS
を true に変えるとAPI層がモックに差し替わる仕組みになっています。
どのように storyboad / xib を使っているか
セオリー通り、 1つのViewController に対して 1つのstoryboadで、基本的に全てのUIViewController, UIVIew, Cellに対してstoryboard or xibが存在していました。
ちなみにモダンにStackViewを使用しています。
全てのStoryboardは enum で管理されていて、enumからインスタンス化していました。
どこまでをStoryboadで定義しているか
storyboard / xib では主にレイアウト情報を定義しているだけでした。 フォントや色などのスタイルの定義はコードで設定しています。
最もポピュラーなやり方だと思う
やはりkicksterterのように storyboad / xib を主にレイアウトのみで使うことで レイアウトのコードを省きつつ、Viewの構造を理解しやすくする。フォントや色はアプリ全体で統一した値を使うためにコードで書く。
というのが多分 現状では一番ポピュラーなやりかたなんじゃないかと僕は思っています。(意見ください)
(今後は、Xcode9で追加されるAssets catalogsのnamed colors supportなどによってアプリで統一している色やフォントもInterfaceBuilder上で定義しやすくなってこの辺もstoryboardで定義するのが良くなっていくのでしょうか。)
独自のライフサイクルを追加している
kicksterterではUIView
とUIViewController
に独自のライフサイクルメソッドである bindStyles()
と bindViewModel()
を追加していました。
独自のライフサイクルといっても、もともとあるライフサイクルと同じタイミングで特定のメソッドを呼ぶようにしているだけです。 実装を見るとわかりますが、method swizzling か extensionを使ったオーバーライドによってそれを実現しています。
スタイルを定義する場所とViewModelとのバインドを定義する場所が決まっているとプロジェクト全体で統一感がでて綺麗になるし実装する時に迷わずにすむので良さそうです。
独自のライフサイクルメソッド
bindViewModels()
: ViewModelをバインドする処理を書くところbindStyles()
: Viewのフォントや色などを適応するところ
それらが呼ばれるタイミング
UIView
bindViewModels()
-> awakeFromNibbindSytles()
-> traitCollectionDidChange
UIViewController
bindViewModel()
-> viewDidLoadbindStyles()
-> traitCollectionDidChange + viewWillAppear(初回のみ)
基本的にどちらもほぼ初期化のタイミングで呼ばれます。
traitCollectionDidChangeでbindSytles()してるのが新鮮でした
このメソッドは本来 画面回転のイベントをフックするタイミングで呼ばれるので、ここでスタイルの適用をしているのが個人的には参考になりました。
traitCollectionDidChange呼ばれるタイミングは以下です。
UIView の traitCollectionDidChange が呼ばれるタイミング
- 自身が
addSubview
された時 - 画面回転時
UIViewController の traitCollectionDidChange が呼ばれるタイミング
- アプリケーションの起動時
- ViewControllerが初めてロードされたタイミング
- 画面回転時
traitCollectionDidChangeが呼ばれるタイミングは画面回転時だけではなく、初期化時にも呼ばれるので確かにスタイルを設定するタイミングとしては適していそうです。
また、bindViewModelsのタイミングとしてawakeFromNibを使う辺り、徹底して全てのViewでInterfaceBuilderを使用する方針が伺えます。
ViewModelの書き方がイケてる
KicksterterのViewModelでは、inputs
と outputs
を protocol として定義しており、
「入力」と「出力」が明確に別れて列挙されており大変シンプルでわかりやすいインターフェイスを実現していました。
これはとても分かりやすいと思ったので自分のプロジェクトでも導入を検討しています!
protocol MyViewModelInputs { // タップや文字入力などを受け取るメソッドたち } protocol MyViewModelOutputs { // Viewがobserveするプロパティたち } // 使用側はこの型を使用することでシンプルに扱える protocol MyViewModelType { var inputs: MyViewModelInputs { get } var outputs: MyViewModelOutputs { get } } final class MyViewModel: MyViewModelType, MyViewModelInputs, MyViewModelOutputs { // 本体クラスはよしなにinputs, outputsのインターフェイスを実現 }
実際のコードはこんな感じです: CommentsViewModel.swift
また、Swiftはヘッダファイルがないので、こういったアプローチをすることでもヘッダファイルのようなインターフェイスを実現することができるという気付きも新鮮でした!
詳細は@muukii0803さんの「Kickstarter-iOSのViewModelの作り方がウマかった 」をご覧ください。
MVVMで画面遷移
画面遷移の方法が参考になりました。
- ViewModel側に showHoge のような Observableを公開しておき、
- ViewControllerがそれを監視して画面遷移します。
- (次の画面生成に必要なオブジェクトはObsesrvableで流れてきます。)
self.helpViewModel.outputs.showWebHelp .observeForControllerAction() .observeValues { [weak self] helpType in self?.goToHelpType(helpType) }
詳細は@star__hoshiさんの「[MVVM] kickstarter/ios-oss での画面遷移のやり方」をご覧ください。
独自の演算子を使用している
|>
, ||>
, ?|>
, •
, >>>
, <<<
, <>
,
.~
, ^*
, •
, ..
, >•>
, %~
, %~~
, <>~
elixirにもあるパイプライン演算子 |>
しか読めませんでしたが、確かにこれは便利かもしれません。
演算子を使わない場合と比べると、とてもシンプルに書けているのではないでしょうか。
let backing = .template |> Activity.lens.id .~ 62 |> Activity.lens.project .~ (.cosmicSurgery |> Project.lens.photo.med .~ "" |> Project.lens.photo.full .~ "" |> Project.lens.stats.fundingProgress .~ 0.88 ) |> Activity.lens.user .~ (.template |> User.lens.name .~ "Judith Light" |> User.lens.avatar.small .~ "" ) |> Activity.lens.category .~ .backing
↑ ActivitiesViewControllerTests.swift
所見ではマジでイミフです。 けど、慣れれば記述力と可読性が上がるのだと思います。
public let activitySampleCellStyle = baseTableViewCellStyle() <> UITableViewCell.lens.backgroundColor .~ .clear <> UITableViewCell.lens.contentView.layoutMargins %~~ { _, view in view.traitCollection.isRegularRegular ? .init(top: Styles.grid(4), left: Styles.grid(30), bottom: Styles.grid(3), right: Styles.grid(30)) : .init(top: Styles.grid(4), left: Styles.grid(2), bottom: Styles.grid(3), right: Styles.grid(2)) }
演算子の定義をしてるファイルはこちら
いいかも
プロジェクトの学習コストは少しは上がりますが、とてもスッキリ書けそうです。 学習コスト上がるとはいえ超短期でプロジェクトに入るエンジニアなんて滅多に居ないので独自の演算子の導入は検討してみても良いかもしれないなと思いました。
あとがき
随所で参考になるのでなかなか読み応えがありました。 まだ完全に読み込めてないのでもう少し読んでみて、もう1本記事を書きたいと思います。
他にも mozilla-mobile/firefox-ios 等も読んでいきたいと思いました。
間違いの指摘等ございましたら気軽にコメントよろしくお願いします🙏