iOS SwiftUI mobile

minne iOSの2021年の開発環境

iOS SwiftUI mobile

minne事業部モバイルチームのシニアエンジニアをしております、@joshです。久しぶりに日本語の長文を書いています。

minneのiOSアプリを開発しており、直近の半年間は、iOS 12のサポートを終了し、SwiftUI, GraphQL, Combineなど、チームで多くの面白い新技術を活用して開発効率を上げているので、説明したいと思います。

開発体制

まず、技術以前に「人」です。minneは、iOSエンジニア4名で開発しており、デザイナー、ディレクター、ほかのエンジニアと協力して、機能追加と保守を行っています。コミュニケーションは主にSlack, GitHub Enterprise, Google Meetで行っています。ちなみに、当社はテレワークを基本とする勤務体制へ移行したので、完全リモートの環境です。

Project

基本のバージョン管理ツールは定番のGitとGitHub Enterpriseです。また、iOS限定について話すと、去年個人のブログでこの辺りについて説明していますが、以下のツールを使っています。

ツール 目的
XcodeGen pbxprojの生成(神ツールです)
SwiftLint + SwiftFormat lint + format
quicktype エンティティの自動生成
Scaffold 拙作ですが、ViewModel/Test/ViewModelのテンプレート生成

開発言語と主要フレームワーク

95%以上Swiftで、特に古いテストやエンティティはObjective-Cが少しだけあります。新規のものは必ずSwiftで開発していて、残っているObjective-Cは少しずつ減らしています。

使っているSDKのバージョンに関しては、Xcode 12への移行対応が終わっており、近日中に12でビルドしたバイナリーを公開する予定です。

ほぼほぼUIKitですが、いくつかの画面でSwiftUIを使っています。最低対応のSDKはiOS 13なので、LazyStack系やLazyGrid系がまだ使えないのですが、設定周りの画面など、CollectionViewっぽくない画面を今年中にたくさん移行しようと思っています。移行自体は色々考えてプロトタイプを作るなどして、2020年のiOSDCで発表しました。また、UIKitはモダンなAPIを使うように心がけており、例えば、UICollectionViewを使っている新規画面でだいたいDiffableDataSourcesCompositionalLayoutを使っています。

ログ周りは、Google Analytics for FirebaseReproなどをPuree-Swiftでラップして、統一したインターフェイスに移行している途中です。

アーキテクチャ

Objective-CがたくさんSwiftと共存していた時代は、RxSwiftなどのFRPライブラリを導入することにチームとして抵抗があったので、MVPのGUIアーキテクチャでしたが、SwiftUIとCombineが発表された瞬間からSwiftUI移行のしやすくさを考慮し、MVVMやReduxなどを検討しました。現在はCombineを使ったMVVMをGUIアーキテクチャに採用しておりますが、画面間のデータフローが実装しやすいThe Composable Architecture (TCA)を最近検討しており、試験的に導入し、評価する予定です。また、モデル層について少しずつ議論して、パターン化しています。

Test

テストをできるだけ書きやすくするために以下のツールを活用しています。

社内ではRuby on Rails経験者が多いというのもあって、RSpecに近い、やさしい書き方ができる QuickNimbleを使っています。ただ、XCTestを使いたい人は、使って良いというスタンスで、柔軟で平和な日々です。

依存性の注入(DI)はライブラリを利用せず、普通のコンストラクタインジェクションですが、このめっちゃくちゃ便利なProtocol Compositionパターンに助かっています。

Mockは型付けの強いSwiftでは普段作るのが面倒ですが、SourceryのAutoMockableテンプレートを活用して、自動生成しています。

CombineのコードはXCTestExpectationだけでは、以下の問題があるので、CombineExpectationsCombineSchedulersを使ってテストを書いています。

  1. debounceなどのオペレーターでschedulerを注入しないと、テストが遅くなってしまう
  2. 記述量が多い

CI/CD

この辺りは特に力を入れており、ツールとしてFastlane + Dangerを積極的に活用しており、サービスとしてBitrise + TestFlightを使っています。方針として、できるだけ多くのことを自動化することによって、開発とレビューに割ける時間を最大化し、アウトプットの量を増やし品質を向上できると考えています。

Fastlaneはもやはモバイル開発の定番の自動化ツールで、スクリーンショット作成以外は、メインのアクションをすべて活用しています。

  • アプリのベータと本番用ビルドを配信
  • App Storeメタデータを管理
  • プッシュ通知証明書やProvisioning Profileの更新
  • CI上のテスト実行

私自身danger/swiftのcontributerですが、恥ずかしいことにまだRuby製のDangerを使っています。Dangerを使って、簡単なところを自動化して、レビューで不具合がないかや保守しやすいかどうかなど、高度なところを我々人間でやっております。(人間いぇ〜い)

  • レビュアのアサイン
  • デバッグコードが残っていないかの検知
  • Storyboardのlint
  • タイポの検知(社内唯一の英語ネイティブなので、これで自分からの指摘がめっちゃ減って幸せになった)
  • ビルドとテスト結果の投稿
  • コードカバレッジの投稿

近い将来SwiftLint実行やラベル管理など、色々GitHub Actionsに移して、CIをより高速化しようと思っています。また、SwiftInfoを導入し、コードベースの変化をより見える形にしようと思っています。

技術課題

技術課題は、ほかのiOSアプリもよくあるものだと思いますが、minneは、Objective-Cで2012年から開発されており、画面が120個以上あるので、ビルド時間が長く、そしてFat ViewControllerがまあまああります。ビルド時間は基本的にモジュール分割で、Fat ViewControllerはリライトで取り組んでいます。(ビルド時間問題は、Apple SiliconのハイスペックなMacBook Proが出たら、「銀の弾丸」であるお金で解決したいです。)

あえて導入していないもの

少し意外かもしれませんが、minneではAlamofireをやめて通信ライブラリや通信のモックライブラリ(OHTTPStubsなど)を一切使っていません。今の時代は、あまり通信ライブラリが必要ないと感じており、また、FoundationのURLSessionを使うのに以下のメリットがあります。もちろん、その反面パラメータエンコードと画像アップロードに使うmultipart/form-dataを自前で実装するのはちょっと面倒でしたが、minneは大量のエンドポイントを扱い、ネットワーク処理がわりとキモなので、通信処理を内製化すると、より精密にそのレイヤーをコントロールできます。また、徐々にGraphQLに移行しているので、その辺りの通信処理を一部apollo-iosに託しています。

  • レイヤーが減るので、デバッグもしやすくなる
  • アップデート対応がなくなる
  • 依存性が減る

個人的にかなり興味を持っているのですが、BuckBazelなど、xcodebuild以外のビルドシステムは4人体制ではあまり保守できそうにないので、採用していません。

ほぼほぼやめて、あと一歩でなくなるツールやライブラリに関しては、Carthage, RxSwift, JSONの変換ライブラリがあります。Carthageは事前にビルドするので、簡単にキャッシュできるというのが非常に大きいメリットですが、Xcode 12では使いにくく1、Swift Package Manager(SPM)がやっとバイナリーやアセットリソースを使えるようになったので、できるだけSPMに一本化したいです。RxSwiftはあまり多くはないですが、Combineでリプレースしています。JSONの変換ライブラリも標準のCodableでだいたいなくしており、もう少しで消えます。

まとめ

minne iOSアプリでの開発状況です。モダンな環境でSwiftUI、Combine、GraphQLを積極的に増やして、新機能も定期的に追加することにご興味あれば、ぜひ応募してください!また、質問や疑問などあれば、ぜひTwitterで突っ込んでください〜

  1. Xcode 12から、iPhone実機のarm64アーキテクチャバイナリーに加えてApple Silicon向けのarm64バイナリーもビルドされるようになり、リンク時にiPhone実機とmacのそれぞれのarm64バイナリーを同一フレームワーク内に含むことができずエラーとなるので、ワークアラウンドが必要になってしまい、使い勝手が悪くなりました。最新の状態と詳細はIssueドキュメントをご参照ください。