Retty Tech Blog

実名口コミグルメサービスRettyのエンジニアによるTech Blogです。プロダクト開発にまつわるナレッジをアウトプットして、世の中がHappyになっていくようなコンテンツを発信します。

22卒の私が1年間行ったRetty iOSプロジェクトの改善

こんにちは

アプリ開発チームでiOS開発をしているレイです。

約1年前の記事では、CI/CDサービスの切り替えについて紹介しました。

engineer.retty.me

その後も、私は日々の施策開発以外にも多くの技術的な課題を解決してきました。今回はこの一年間に取り組んだ改善点について、簡単に紹介したいと思います。

1. Swift Packageへの移行

ライブラリ、開発ツールの移行

約1年前に、BitriseからXcodeCloudにCI/CDを移行しました。その際、CocoaPodsやMintなどの外部ツールで管理していたものをSwift Packageに移行する作業を始めました。

ライブラリの数が多かったため、半年ほどかけてSwift Packageに移行し、メンテナンスされていないライブラリについては自分たちで新たに作り直しました。

現在、CocoaPodsで管理していたすべてのライブラリがSwift Packageに移行し、また、MintやHomeBrewで管理していた開発ツール(SwiftLint、SwiftFormatなど)も同様にSwift Packageのプラグインに移行しました。

さらに、XCFrameworkやartifactBundleの利用も積極的に増やしました。AWSやFirebase関連のフレームワークはXCFrameworkで管理し、LicensePlistやSwiftFormat、SwiftLintはartifactBundleを利用するようにしました。

この作業を通じて、XcodeCloudでフレームワークや開発ツールのキャッシュを活用できるようになり、XcodeCloud導入時と比べて約2倍の速度改善が実現しました。(テスト用CI、リリース用配信、検証用配信: 約30分 → 約15分)

また、以前はプロジェクトをビルドするためにRubyやCocoaPodsなどのセットアップが必要でしたが、今回の改善で外部ツールに依存しない形になり、Git Clone後すぐにビルドできるようになりました。

マルチモジュール化

Retty iOS開発ではこれまで、マルチモジュール化をあまり考慮せずに進めていました。チーム内ではモジュール化が何か良くなることは理解していたものの、具体的な方針が明確でないまま進んでいました。

個人的にこの点が課題だと感じており、自分が作成したアプリプロジェクトを通じて、何が良いのか、何が必要なのかについてチームで話し合うため、マルチモジュール化に関する会議を開催しました。その際には、POやプランナーの方々ともこのプロジェクトの必要性についてドキュメント化や相談を行い、納得していただくよう努めました。

整理したドキュメントの一部

関係者の同意を得た後、以下のような方針で進めています。

  • 新規の画面については、基本的にSingletonではなくDI(Dependency Injection)を導入し、モジュール化を考慮した構成にしていく
  • 旧画面については、大きな画面(ホーム、店舗詳細、検索)について段階的に改善を進める

その結果、現時点(2023/11)ではホーム、検索+検索結果、投稿作成画面がDIとモジュール化されました。

DIやアーキテクチャなどのモジュール化の手法は、チームで共有した個人プロジェクトを元にしてRetty iOSに適用しています。詳細については、このレポジトリをサンプルとしてご参照ください。

github.com

2. テスト周りの技術課題の解決

Rettyアプリチームでは、今年からQAの負担や不具合を減らすためにテスト駆動開発(TDD)を導入し始めました

しかし、テストを作成する際に以下のような課題がありました。

  • テストの結果が意図しない値になり、テストが失敗することがある
  • テストの実行速度が遅い

これらの問題の原因は、次のような点でした。

  • Unit Testを実行する際にHost Applicationで実行していたため、アプリの起動に時間がかかっていたこと
  • テスト時に本番のAppDelegateを使用しており、不要な処理がテストに影響を与えていたこと

上記の問題を解決するために、テスト用のAppDelegateを作成し、それによってAppDelegateのモック化を実現し、問題を解決することができました。

// main.swiftで本番のAppDelegateとAppDelegateMockを適用するロジック
let appDelegateClass: AnyClass
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
    appDelegateClass = AppDelegate.self
} else {
    appDelegateClass = NSClassFromString("AppDelegateMock") ?? AppDelegate.self
}
UIApplicationMain(
    CommandLine.argc,
    CommandLine.unsafeArgv,
    nil,
    NSStringFromClass(appDelegateClass)
)

ただし、Host Applicationを使用しない形にするにはRetty iOSにはいくつかの課題がありました。

通常のケースでは、ソースコードフレームワークなどをTest Targetにリンクすることで解決されます。

しかし、Retty iOSでは一部ライブラリを直接importする形で利用しているため、直接リンクされたフレームワークが存在しました。

そのため、以下のような作業が少し追加で必要でした。

  • Test TargetのLink Binary With Librariesに必要なバイナリファイルを追加する
  • Header Search Pathsに$(CONFIGURATION_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/Headersを追加することで必要なヘッダーをリンクする
  • Test TargetにもBridging Headerを追加し、テストのBuildSettingsにもPathを追加する
  • フレームワークのヘッダーをPublicに変更する

これにより、テストの実行時間が安定化し(約2〜3分 → 1分)、常に意図したテスト結果が得られるようになりました。

3. クラッシュ改善

Retty iOSでは、以前からCrash Free Rate(CFR)がかなり低い課題が存在していました。対応可能なものはすぐに対処してきましたが、数年間解決できなかったクラッシュがCFR全体の数値を低く保つ原因となっていました。

Firebase Crashlyticsのログ一部

主にCFNetworkやCoreFoundation周辺での報告があり、通信関連の問題だと特定されましたが、具体的な問題点は明確ではありませんでした。

当時、Retty内で使用していた通信SDK(URLSessionベース)とクライアント内での通信コード(Alamofireベース)がありました。最初はAlamofireの使い方に問題があるのかと考え、URLSessionへの切り替え作業を行いましたが、効果が見られませんでした。この時点でSDKに問題があることが明らかになりました。

問題の再現方法が分からない中、私はXcodeのProfileやDiagnostics機能を活用し、SDK内で変数のaccess-raceが発生していることを見つけ出し、解決に至りました。

その結果、数年間95〜96%だったCrash Free Rate(CFR)が、現在は99.8〜99.9%まで改善され、ユーザーの満足度を向上させることができました。

(この時、レイはUser Happy賞を受賞しました。😉)

社内表彰での受賞

4. メモリ利用量改善

Retty iOSアプリでは、これまで画像関連で大量のメモリを使用しており、それによるさまざまな問題がありました。

iOS端末はメモリがAndroidと比較して少ないため、メモリの適切な使用が重要で、不適切な利用はアプリのパフォーマンス低下やクラッシュを引き起こす可能性があります。

これまでのRetty iOSでは、画質を下げたり、メモリの利用量に制限を設けたり、キャッシュを適切に管理するなどの対策をしてきましたが、画質を下げることはユーザー体験に影響するため、望ましくありませんでした。

以前の画像メモリキャッシュ改善記事はこちらでご覧いただけます。 engineer.retty.me

そこで、私はアプリで表示される画像に対して全体的にdownsamplingを適用し、ローカルの写真もリモートで取得した画像と同じようにキャッシュやdownsamplingを実施しました。

iOSで画像をdownsamplingやキャッシュする方法はいくつかあります。
downsamplingの場合、Apple公式のImageIOを自前で利用する方法や、キャッシュの場合はNSCacheを使用する方法などがあります。
ただし、Retty iOSではリモートの画像表示にKingfisherを導入しているため、これを利用してリモートの画像だけでなく、ローカルの画像(PHAsset)にも同様の処理を適用しました。

これにより、アプリ全体でKingfisherのキャッシュとDownsampling Image Processorを利用することで、次のような効果が得られました。

  • 解像度がほぼオリジナルに近い状態で画像を取得でき、アプリで表示する画像がより鮮明になりました
  • メモリの使用量が300〜600%も削減され、アプリのパフォーマンスが大幅に向上しました
  • これまでメモリの不足によって実現できなかった施策が可能になる状態になりました

おわりに

今回の記事では、私の一年間における施策開発以外での技術課題解決に焦点を当てました。

もちろん、記事では触れられなかったアーキテクチャーの改善やConcurrencyの導入などもありますが、今回はその中で効果が特に大きかったものを紹介しました。

これからも、自身の成長とチームの最高のパフォーマンスを引き出すための開発環境構築やUser Happyのために、さまざまな挑戦を続けていきたいと考えています。