スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Template設計から始めるiOS開発

はじめまして、今年5月に入社したモバイルエンジニアの@stNamcoです。

今回はコーディング規約周りの取り組みを記事にしたいと思います。

TL;DR

  • 複数のプログラマが参加するプロジェクトにおいては、プログラミング品質を均等にするためcoding規約を定めておくことが必要になる。
  • coding規約は、そもそもドキュメントであるという性質上、実質的には読む読まないは選択可能であり、仮に読まずに作業が勧められたPRが出された場合、そのPRを突きかえすというのは、レビュワー的にもストレスになる。
  • 以上のことから、コード規約ではなく、Templateや、CodeFormatterを事前に定義しておくことで、自動的に同品質のコードを作っていけることを目指した。

利用したツール

今回解決に取り組んだ点

  • 構成の共有
    • xctemplate
      • 基盤となるモジュールの一括作成。
    • sourcery
      • 実装されたEntityを元に、repositoryを自動生成。
  • コーディングスタイルの統一
    • SwiftLint
      • 可読性向上、危険なコードの排除を目的とし記法を統一。
    • SwiftFormat
      • 可読性向上を目的とし記法を統一。

実装

Qiitaのユーザ名をリスト表示するアプリを実装しながら、利用イメージをお伝えしたいと思います。

stNamco/QiitaExample

  • 画面
    • ViewController. Providerで構成
  • ドメイン
    • usecase, repository, entityで構成

※ここでは各種ツールの詳細の使い方については触れません。前述のリンクをご確認ください。

事前準備

プロジェクトを作成し、podを使えるようにします。podの設定には下記を加えておきます。今回はpodを利用していますが、Carthageでも大丈夫です。

def formatter
  pod 'SwiftLint'
  pod 'SwiftFormat/CLI'
  pod 'Sourcery'
end

SwiftLintを設定

SwiftLintをRunScriptに追加します。ruleの設定内容に関しては、基本的にデフォルト設定のruleを利用しつつ、opt-inでカスタマイズしていくという形になると思います。

opt-inの設定は、設定することでperfomanceが落ちるものもあるようなので、必要なものだけ設定していくのが良さそうです。

具体的には、performance kindのopt-inを設定し、あとはチームによってカスタマイズしていくようにしていくのがいいと考えています。

Guidelines on when to mark a rule as opt-in: • A rule that can have many false positives (e.g. empty_count) • A rule that is too slow • A rule that is not general consensus or is only useful in some cases (e.g. force_unwrapping)

excluded:
 - Pods/
 - Carthage/
 - Crown/Resources/R.generated.swift
 - Scripts/

line_length:
- 300   # warning
- 400   # error

type_body_length:
- 400   # warning
- 600   # error

file_length:
- 400   # warning
- 1000  # error

function_body_length:
- 100   # warning
- 200   # error

opt_in_rules:
- closure_end_indentation
- contains_over_first_not_nil # kind - perfomance 
- discouraged_optional_boolean
- empty_count # kind - perfomance 
- empty_string # kind - perfomance 
- first_where # kind - perfomance 
- force_unwrapping
- literal_expression_end_indentation
- multiline_parameters
- operator_usage_whitespace
- overridden_super_call
- override_in_extension
- private_action
- redundant_nil_coalescing
- sorted_first_last # kind - perfomance 
- switch_case_on_newline
- untyped_error_in_catch
- vertical_parameter_alignment_on_call
- yoda_condition

disabled_rules:
# SwiftFormatを利用することで空白は削除できる。SwiftFormatで'--trimwhitespace nonblank-lines'を利用し、空行のインデントを残しているので、trailing_whitespaceはdisabledにしておく
- trailing_whitespace 
- large_tuple

identifier_name:
  min_length:
    warning: 1

SwiftFormatを設定

SwifftFormatではcodeのformating設定を行います。各種設定は、前述のリンクから確認してください。

ちなみに--inferoptionsオプションをつけることで、既存のprojectから自動的にruleを生成することもできるようです。ただ、自分が実行した時は、実装コード的に期待していないルールまで反映されてしまったので、そのまま適用するよりも、たたきにするくらいが良さそうです。

  1. Many configuration options are exposed in the command-line interface or .swiftformat configuration file. You can either set these manually, or use the --inferoptions argument to automatically generate the configuration from your existing project.

--inferoptionsを利用してruleを生成

実行結果

Pods/SwiftFormat/CommandLineTool/swiftformat . --exclude Carthage,Pods --allman false --wraparguments beforefirst --wrapelements beforefirst --self remove --header ignore --binarygrouping none --octalgrouping none --indentcase false --trimwhitespace always --decimalgrouping none --patternlet inline --commas inline --semicolons inline --indent 4 --exponentcase lowercase --operatorfunc spaced --elseposition same-line --empty void --ranges spaced --hexliteralcase uppercase --linebreaks lf --hexgrouping none --comments indent --ifdef indent --stripunusedargs closure-only

次に、CodeFormatterを実行するためのスクリプトを追加します。ビルド時に適用することもできますが、個人的には変更された部分を確認しておくべきだと思うので、gitフックのpost-commitを利用し、commit後にスクリプトが実行されるようにしています。

This hook is invoked by linkgit:git-commit[1]. It takes no parameters, and is invoked after a commit is made. This hook is meant primarily for notification, and cannot affect the outcome of git commit.

そうすることで、変更対象のファイルが存在していれば、formatで変更された状態のワーキングツリーが作られますし、変更対象のファイルがなければ、何も変更が加えられていない状態のワーキングツリーになります。

またSwiftFormat実行時に、合わせて以下のようなファイルソート用のスクリプトも同時に実行することで、ファイル順も一意になるようにしています。 sort-Xcode-project-file

xctemplateを設定

templateを読み込むためのスクリプトを追加します。xctemplateに関する記事の中には、ディレクトリも含めプロジェクトの構成を全て行なってしまう。。みたいな実装もありましたが、個人的には、対象プロジェクトのみで利用することを想定したモジュール生成のtemplateが良いと思います。

理由は下記に記載しました。 - 不特定の実装を想定したtemplateなら、今回の目的であるコードの規約的な役目を果たせない。 - 結局プロジェクト書き加えるべき項目が残ってしまうため、作業的にも手間がかかってしまう。 - 構成はプロジェクトの仕様や、技術的なトレンドもあるので、都度考えるべき。

上記のような背景から、templateを含むディレクトリ名はプロジェクトの名前にしています。(今回でいくとQiitaExample)

またtemplateはxcodeに紐づいて保存されるため、当然別プロジェクトを開いた際にも表示されます。そのため、不要になったtemplateはこまめに消しておくことをお勧めします。

今回はScreen.xctemplateとDomain.xctemplateを作りました。templateの作り方はそれなりの文量になってしまうためここでは割愛します。googlabilityの低さゆえか、公式のドキュメントを見つけることはできませんでしたが、前述のリンクが参考になりました。

# Configuration
CUSTOM_TEMPLATE_DIR_NAME='QiitaExample'
CUSTOM_SCREEN_TEMPLATE_DIR_NAME='Screen'
XCODE_TEMPLATE_DIR=$HOME'/Library/Developer/Xcode/Templates/File Templates/'$CUSTOM_TEMPLATE_DIR_NAME
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/Scripts/templates"

createTemplate () {
  echo "==> Creating Xcode file templates..."
  echo "==> $SCRIPT_DIR"

  if [ -d "$XCODE_TEMPLATE_DIR" ]; then
      rm -R "$XCODE_TEMPLATE_DIR"
    fi
    mkdir -p "$XCODE_TEMPLATE_DIR"

    cp -R $SCRIPT_DIR/*.xctemplate "$XCODE_TEMPLATE_DIR"
    
    cp -R $SCRIPT_DIR/$CUSTOM_SCREEN_TEMPLATE_DIR_NAME.xctemplate/ownsView/* "$XCODE_TEMPLATE_DIR/$CUSTOM_SCREEN_TEMPLATE_DIR_NAME.xctemplate/ownsViewwithXIB/"
    cp -R $SCRIPT_DIR/$CUSTOM_SCREEN_TEMPLATE_DIR_NAME.xctemplate/ownsView/* "$XCODE_TEMPLATE_DIR/$CUSTOM_SCREEN_TEMPLATE_DIR_NAME.xctemplate/ownsViewwithStoryboard/"  
}

createTemplate

echo "==> ... success!"
echo "==> Template have been set up. In Xcode, select 'New File...' to use Custom templates."     

xctemplate

Sourceryを設定

Sourceryを導入しコードの自動生成を可能にすることで、ボイラープレートコードを記述する必要がなくなります。またリファクタリングやファイル追加の際にも、編集するファイルを絞り込めるので、人為的なミスを排除できます。

まずtempalteを元にファイルを生成するために、下記のようなscriptを追加します。公式exampleではRunScriptで実行されていましたが、以下のような理由から手動で実行しています。 - 生成されるコードに齟齬がないか確認するため、明示的に生成したい。 - templateを更新するため、試し書きで生成されたコードを修正するタイミングもある。その際自動で生成(自動で書き換え)されたくない。

Pods/Sourcery/bin/sourcery --sources "QiitaExample" --templates "QiitaExample/Supports/Templates" --output "QiitaExample/Supports/CodeGenerated"

生成したいtemplateは.stencilのファイルで定義できます。Sourcery/docs/にjazzyで生成されたガイドが準備されているので、詳細な書き方はそちらでご確認ください。

今回の実装では、sourceryを用いて、entityが追加された際にrepositoryが生成されるようにしました。全てのentityに対して、repositoryが必要なわけではないため、AggregatedModelTypeというprotocolに準拠したentityに対してのみ生成しています。

生成されたコードはプロジェクトに追加しないと参照できないので注意してください。

Model+Repository.stencil

2018-10-31 22 06 28

Model+Repository.generated.swift

2018-10-31 22 06 41

まとめ

SwiftLintやSwiftFormatのようなツールはもちろん、Templateも規約の一つとして捉えることで、より具体的な実装イメージまで、メンバー間で共有できるようになったと感じています。

特にQuipperは、Handbookにもあるように、海外籍のエンジニアも多いので、言語的なコミュニケーションでの齟齬を防ぐという意味でも、規約をコードで縛ってしまうというのは有効に感じています。

ちなみに、Sourceryに関しては今回規約という文脈で利用しましたが、Testの補助やTrackingコードの実装などにも利用できそうだと感じたので、今後も色々試してみようと思います。