肥大化したiOSアプリを課題解決のために半年弱でリアーキテクチャした話


## はじめに

2019年に約半年弱の開発期間で公開中のiOSアプリを
リアーキテクチャーした話を備忘録としてまとめました。

サービスインされているアプリを一気に作り直すケースは
ビジネス判断的にはかなり危い橋を渡っているのであまり無いとは思いますが
似たような状況で開発に臨んでいる方など、どなたかの参考になれば幸いです😃

## 開発の背景

* 2014年から毎年追加改修が走っているストア公開中のECアプリ案件(Objective-C)
* 画面数で見れば中規模から大規模にカテゴライズ
* iOSのバージョンについて古いものは毎年切り捨ててく方針
* 開発メンバーは毎年ほぼ総入替
* 自分は2018年頃から参加しました

## 抱えていた課題・技術的負債

受託案件あるあるですが以下の課題を抱えていました。

0️⃣ ピュアObjective-C
1️⃣ 可読性の低い巨大すぎるMVC 
(MassiveViewController + 非同期処理通知はNSNotificationのみ, BlocksKit等の利用無し) 
2️⃣ 同じ/似たような機能を再利用せずにコピペで複数箇所に実装
3️⃣ オンメモリでも全く問題ない不要なアプリ内DB(CoreData)
4️⃣ テストコード・内部設計書の無い長期運用
5️⃣ 上記の問題による運用・改修コストの増加

特に 1️⃣の影響が大きく
非同期処理の通知を受け取る箇所が広範囲に点在されていた為に
毎年入れ替わるエンジニアへの引継ぎや改修時の影響範囲の調査に
余計なコストが掛かっていました。

## 課題を解決するためのアプローチ

### 求められるアーキテクチャーの決定

当時の開発チーム全員で選定しCleanArchitectureで設計しました。

以下の記事でほぼ同じ構成のサンプルプロジェクトを確認可能です。
[Cookiecutterを使って爆速でiOSプロジェクトを作成する](https://qiita.com/satoshin2071/items/82777dee4dbad5f04f94)

#### 採用理由
課題解決のため責務の明確化と疎結合性の向上に加えて
長期的な運用と開発者がコロコロ替わるのに対応するため

#### 他の候補の見送り理由
* MVP => Datasourceを追加したとしてもPresenterが肥大化しそうだった為
* MVVM => RxSwiftの理解が必須なため、エンジニアの入れ替えが激しい要件にそぐわない
* VIPER => 後述のpythonを利用した旧プロジェクトから新プロジェクトへ変換が容易だった為見送り

#### その他

PEAKSから発行されていた [iOSアプリ設計パターン入門](https://peaks.cc/books/iOS_architecture) を輪読していたのも要因です

>iOSアプリ設計パターン入門 Clean Architecture 10.4 p213 より
>
>言い換えると、Clean Architectureはアプリが大規模で長命になるほど恩恵を受けるアーキテクチャと言えます。変化の激しい部分は容易に切り替えが可能ですし、GUI アーキテクチャではModelでまとめられていた役割をさらに分離する指針にもなります。複数人での開発や テストにおいても戸惑うことなく、整然と進められることでしょう。

### 開発メンバーと役割分担

```
👨🏻‍🚀 : Photo, ItemList, CustomerSetting, XCTest
👨🏼‍🔬 : Home, SideMenu, Cart, Payment, CodeConverter(Python)
👨🏼‍🚒 : Photo, PhotoList, OCR
🧛🏻‍♀️ : 途中で交代して引継ぎ Photo, CIFilter・XCUITest
Me : DI, Launch, PushNotification, DeepLink, Routing, CI
```

### CleanArchitectureのディレクトリ構成と各ファイルの責務

[iOS開発でClean Architectureを採用した際のイイ感じのディレクトリ構成とは](https://qiita.com/yamshta/items/de7ea5fc2594b8da5988) 
を参考にData, Scenes といったディレクトリに分離させました。

#### Data
担当したアプリは **1画面1APIのようなI/F設計にはなっていない** ため画面に紐づく構成よりも
独立させて横断できる構成の方が把握が楽かなと感じました。
(※階層が若干深いので Xcodeの `command + shift + j` は必須でした)

DataディレクトリにはWebAPIに関連する各 `Entity` `Entityが持つObject`
`Request` `Requestパラメータ` を配置しています。


<img width="413" alt="スクリーンショット 2020-06-03 14.54.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6691/202ebe0d-c418-fcd2-080a-0f9de16a6a50.png">
<img width="437" alt="スクリーンショット 2020-06-03 14.54.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6691/8a97d342-7756-228d-c391-3c5d8eeeadb6.png">
<img width="520" alt="スクリーンショット 2020-06-03 14.54.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6691/0d485842-389f-fdbf-6717-0127fa7c0a04.png">
<img width="576" alt="スクリーンショット 2020-06-03 14.55.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6691/88385272-6370-0611-3cb4-dce509785a6b.png">

#### Scenes

Scenes以下に画面と紐づく名前でディレクトリを切って
各責務を担当するファイルを分けています。

<img width="531" alt="スクリーンショット 2020-06-03 15.09.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6691/91709a11-f3eb-3f24-6628-60dfb5e01e9c.png">

各責務の概要は以下です。

```
■Configurator 
-- ○○Assmbly.swift
Swinject用の DIグラフ解決用のファイル 
Swinject利用方法のサンプルはこちら
https://github.com/SatoshiN303/SwinjectStoryboardSample

■Domain 
--- ○○Protocols.swift
各ファイルのお互いを伝え合うProtocolをまとめて定義しておく
場所はDomain以下でなくてもよかったかも

--- ○○Gateway.swift
APIを叩いたり、Reamlからデータ取得したり 外側とやりとりする

--- ○○Usecase.swift
Gatewayから受け取ったオブジェクトをPresenter等に伝搬させる
Translatorを呼び出してEntityをModelに変換したり

--- ○○Model.swift & ○○Translator.swift
View用にEntityを加工したオブジェクトと EnityからModelへ変換する機構

※ModelとTranslatorを毎回作ってるとさすがに冗長だという意見もあり
Entityの値を加工する必要がある場合のみ作成

■Presentation
-- ○○Presenter.swift
Usecaseから受け取った値をViewへ伝搬させるなど
Presneterが肥大化するような場面では ○○DataSource.swiftを持って分散したり

-- ○○ViewController.swift
基本的にはfinalで。Scenes毎の画面遷移はSegueを使わずにVCにmakeInstanceな
static関数を生やして依存性注入する。
```

### Objective-CのファイルをまとめてCleanArchitecture構造のSwiftファイルに変換するpythonスクリプトを利用

メンバーの提案で開発初期のスピードを早めるために
旧Xcodeプロジェクトをpythonでクロールして
CleanArchitectureの構造に定義されたSwiftファイルを
まとめて出力するスクリプトを利用しました。

Swinjectを利用したDependencyGraphの解決、
どの画面がどのWebAPIを叩いてるかのプロトコルへの紐付けなど、
あらかじめ定義できる静的な部分を生成しています。

### 生成例
<img width="518" alt="スクリーンショット 2020-06-03 16.29.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6691/1744b8cd-0218-8725-3d02-c3535dcb8403.png">

<img width="431" alt="スクリーンショット 2020-06-03 17.55.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6691/779849c7-4060-2f33-30ce-a0f89e8a28e6.png">


私が実装したものではないので、主な処理内容だけ共有します。

```
前提 ディクショナリで 旧ファイル名 : 新ファイル名の変換表を保持
(1) storyBoardに関連付けられてるViewContorllerを取得
(2) 上記のViewControllerのソースコードを読み取り
(3) WebAPIを叩いてるViewController見つけたらディクショナリ生成  (vc名: [WebAPIその1, WebAPIその2]) のような
(4) 変換テーブルを用いてCleanArchitectureの形式でswiftファイルを一括生成 & 必要な定義をプロトコルに書き込んで紐付け
```

### 開発方針・規約的なもの

* Segueは利用しない。画面遷移が発生するVCには `static func makeInstance()`的なものを生やして画面に必要な依存性を注入しつつDeepLink等にも対応可能にする
* RxSwiftについてはエンジニア入れ替え時の負担を軽減するため部分的な利用に留める (Promise的に書きたい箇所やRxCocoa等など)
* [swiftLint.yml はこちら](https://gist.github.com/SatoshiN303/9e651f2c9094ee82861211b1395e70dd) (※記述が古いかもしれません)
* [Swiftformat/CLI](https://github.com/nicklockwood/SwiftFormat) を利用
* 基本はCarthage, 必要に応じでCocoaPodを利用
* 本番、ステージング、開発環境の切り分けはxcconfigで

### CIで行っていたこと

![5e6f9ccc3e129dfd8a205e4e_Bitrise Logo - Eggplant Bg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6691/fc1e2746-0257-2051-285a-ab4e5a9ac691.png)


CIはBitriseを利用していました。
[受託開発での iOS アプリプロジェクト新規作成プラクティス（下編：Bitrise 編）](https://qiita.com/lovee/items/811eae37928447f06b36)
を参考にPullRequestをトリガーにしてDangerやSwiftLintを実行する
最低限のコードレビュー自動化やAdhoc/Releaseビルドの配布を行っています。

### 開発ツール・その他

* [ビルド環境に応じて必要なGoogleService-Info.plistを利用するRunScript](https://gist.github.com/SatoshiN303/14b6dcb9a575b5305c55db80c8ea0b09)
* [CleanArchitectureで必要なファイルをまとめて追加する .xctemplate](https://github.com/SatoshiN303/Mold2)

### 開発で優先しなかったこと

* Xcode11, iOS13対応 => タイミング的に先送り可能な時期だったので
* 充実したテストコード => 動作が不安定だった為、全WebAPIのチェッキングのみ

## 作り直して解決したコト、 ポジティブなインパクト

* 💪 「どの処理がどこで何をしているか」の責務が明確になり運用・改修コストが大体0.5〜1.5人日削減
* 💪クラッシュの影響を受けていないユーザーが繁忙期計測値90%から99％へ上昇&キープ
* 💪Swinject/SwinjectStoryboard によるDIでモック可能なテスタブルで構成に
* 💪引き継ぎコストに1.5人日程度掛かっていた状態を0.5人日に短縮
* 💪注文件数 前年比130% (リアーキテクチャー以外の要因も大きいですが一応…)

## 残っている課題
* DIコンテナがSwinjectに依存しているので脱却
* embed frameworks化
* 端末依存のコード (isIPhoneX的なもの)
* 一部CleanArchitectureの思想から外れてしまったサイドメニューの実装等

## 番外編：リアーキテクチャー決定に至るまでの開発チームとしての根回し的なもの

形としては受託案件でしたので追加改修の工数を見積るタイミングで
以下を合わせて伝えておりました。(※クライアントとの関係性ありきです)

1. 技術的負債の影響で追加改修に掛かる余計なタスク(==費用)を数値化し見積もりに含める
2. 影響範囲の大きい要件は追加改修の影響で今後の開発コストが掛かる可能性も伝える

最初はメイン機能周辺をSwift化できれば開発チームとしては御の字だったのですが
タイミング的にビジネスを拡大して一気に攻めたいというクライアントの意向と重なり
(政治的な根回しもあり)結果的にほぼ全体をリアーキテクチャーするに至りました。

## 最後に

昨年の話なのでSwiftUIやiOS13については特に触れておりませんが
どなたかのお役に立てれば幸いです。
つらつらとした長文でしたが、お読み頂きありがとうございます😎

## 参考文献・URL・スライド

非常に参考にさせて頂きました。ありがとうございます🙇‍♂️

* [Swift 実践入門](https://www.amazon.co.jp/%EF%BC%BB%E5%A2%97%E8%A3%9C%E6%94%B9%E8%A8%82%E7%AC%AC3%E7%89%88%EF%BC%BDSwift%E5%AE%9F%E8%B7%B5%E5%85%A5%E9%96%80-%E7%9B%B4%E6%84%9F%E7%9A%84%E3%81%AA%E6%96%87%E6%B3%95%E3%81%A8%E5%AE%89%E5%85%A8%E6%80%A7%E3%82%92%E5%85%BC%E3%81%AD%E5%82%99%E3%81%88%E3%81%9F%E8%A8%80%E8%AA%9E-WEB-PRESS-plus-ebook/dp/B086VVQ3DJ/ref=sr_1_1?adgrpid=56053864951&dchild=1&gclid=EAIaIQobChMI0oro35Ll6QIVjKmWCh0UmAGGEAAYASAAEgJ9ifD_BwE&hvadid=338518510752&hvdev=c&hvlocphy=1009653&hvnetw=g&hvqmt=e&hvrand=17932127934857333286&hvtargid=kwd-306987067810&hydadcr=27263_11561108&jp-ad-ap=0&keywords=swift+%E5%AE%9F%E8%B7%B5%E5%85%A5%E9%96%80&qid=1591170159&sr=8-1&tag=googhydr-22)
* [iOSアプリ設計パターン入門](https://peaks.cc/books/iOS_architecture)
* [比較して学ぶ RxSwift4 入門](https://booth.pm/ja/items/1010258)
* [Swinject と DIKit で Dependency Injection してみた](https://speakerdeck.com/takasek/20180403-number-roppongiswift)
* [iOSプロジェクトのBuildを高速化する](https://developers.cyberagent.co.jp/blog/archives/7815/)
* [iOS開発でClean Architectureを採用した際のイイ感じのディレクトリ構成とは](https://qiita.com/yamshta/items/de7ea5fc2594b8da5988)
* [受託開発での iOS アプリプロジェクト新規作成プラクティス（下編：Bitrise 編）](https://qiita.com/lovee/items/811eae37928447f06b36)