エウレカはPairs(ペアーズ)のiOSアプリをどのように作っているのか
この記事は「Eureka Advent Calendar 2021」1日目の記事です。
Pairs iOSアプリの開発にあたり、チームの体制やメンバーの動き方、開発の運用、そして実装方針など幅広く触れた内容となります。
エウレカのiOSチームの開発に興味があり、どのように作っていて、今の課題や次にやりたいことは?に関心のある方に向けた記事になります。
幅広く要点に触れつつ、各詳細には触れすぎない記事となりますので、詳しく知りたいという方はMuukii(Hiroshi Kimura)まで連絡をもらえればお答えします。
なお、一番下に私個人として、これからチャレンジしてみたいことが書いてあります。
概要
- チーム体制とメンバーの動き方
- ソースコード管理と申請などの開発における運用
- 具体的な機能実装に関わる設計や技術
チーム構成とプロジェクト
会社の組織構造としてはチームは職種ごとに分けられています。
例
- iOSチーム — iOSエンジニア全員
- Androidチーム — Androidエンジニア全員
職種ごとのチームは運営するサービスごとの定義ではなく、チームがエウレカが開発する全てのアプリケーションの開発を担当します。
例えば、iOSチームはPairs日本版と海外版、そしてPairsエンゲージの開発を担います。
そして、各エンジニアは「プロジェクト」に属し、開発を行います。 プロジェクトはサービスが持つロードマップをもとに設定された特定の目標を持つものです。
プロジェクトは、開発を担当するエンジニアに加えて、進行に必要な職種も加わります。 例えば「Pairsに新しい機能を追加するプロジェクト」の場合、Product Manager(PdM)やDesignerも参加することとなります。
基本的にはこのプロジェクト単位で各機能開発が進みます。 従ってミーティング等はプロジェクトごとに必要に応じて実施されています。
DesignerやPdMが近くにいることでエンジニアもアイデアや意見を共有しやすい、などのメリットがあります。
このように、エンジニアは職種としてのエンジニアチームに所属した形で、特定のプロジェクトにおいて開発を担当するという形になります。
プロジェクトで発生した技術的な懸念などは自身の職種のチームに持ち帰り、エンジニア同士でディスカッションを行います。
ソースコード管理
ソースコードの管理はGitHubにて行います。
- Pairsには日本版と海外版があり、それらは同じリポジトリであり、一つのXcode projectで統合して管理
- 外部ライブラリ管理はCocoaPodsまたは直接埋め込み
- CocoaPodsによる
Pods
ディレクトリはコミットの対象 — 緊急時の対策としてリポジトリにビルドに必要なものをできるだけコミットしておく。 Podのリリースが削除されたりした場合のリスクヘッジ - git-lfsの利用 — 100MBを超えるビルド済みバイナリやリソースの大きなライブラリをコミットするため
branch, tagの運用
- 開発はmain-branchをbaseとして作業branch(いわゆるfeature-branch)を作成
- リリースはmain-branchからリリース用のbranchを作成し、そこからarchiveとTestFlight配信を行い、必要な動作検証を行い、必要な修正を適用
- AppStoreから配信されたバイナリに対応するcommit-hashを使い、リリース用のgit-tagを作成
バージョニング — 基本はmajorをインクリメントする
アプリにおけるバージョンのインクリメント方法はSemantic Versionigに従ってはおらず、順当な更新であればmajor-versionをインクリメントし、リリースされた後のバージョンに対する修正であればminor-versionをインクリメントする。ぐらいの若干の緩さを持ったルールにしています。
つまりは基本的には、ずっとmajorをインクリメントし続けます。
- v1.0.0
- v2.0.0
- v2.1.0 — v2で緊急で必要な修正を適用したバージョン
- v3.0.0
この方式はFacebookやInstagramを参考に考えています。
Semantic Versionigに従おうとしたときに、どのような更新をmajor, minor, patchとするか?という定義はかなり難しいもので、
- 開発者向け?
- ユーザー向け?
- ビジネス上の大きなアップデートだからmajor?
- 大きなリファクタリングを行ったのでmajor?しかし、ビジネス上の変化はないが…
このような迷いや会話が発生することを避けるために単純なインクリメントを採用しました。
Automation
TestFlightへの配信やUnitTestingの実行はCI/CDの構築をGitHub Actionsで行い、できる範囲で自動化を行なっています。
GitHub Actionsにおけるmacインスタンスの利用は利用頻度に対し料金が高額になることからself-hosted-runnerによって実行されています。
今後は料金が安くなるか、そうでない場合はAWSのmacインスタンス上での実行などのクラウド化も考えています。
普段のCIの実行内容として、コミットごとにテストとシミュレータで実行できるアプリのビルドを実行しています。 シミュレータ用のアプリはSlackにアップロードを行い、そのリンクがDetailsボタンより取得できます。
AppStoreConnectにアップロードされたバイナリを申請に提出する時にはGitHub Actionsのworkflow-dispatchから実行します。
実装設計や技術スタック
ここからはアプリの実装の話になります。 ビジネス実現のためにアプリが必要とする機能とエウレカとして目指すべき品質を実現するために採用している技術について幅広く紹介していきます。
まず私が第一に考えていることは
より品質の高いものを、より短い時間で開発し、ユーザーに提供できるようにする
「品質」という言葉は非常に多くの意味を持ちsystem quality attributes に大量の指標が載っていたりします。
その中から選ぶことは難しいですが、私のイメージは次のとおりです。
- 安定し、速い動作 — 遅いより、速く動く方が良い
- 変更の行いやすさ — 自社サービスとして継続的に改善し続けるため
- 開発効率 — 上記を実現するために試行錯誤をできるだけ高速に回せるようにすること
モジュール構成
Pairsアプリはアプリターゲットのみのモジュールではなく、いくつかのモジュールにレイヤーのように分割され縦に積み重なるような構成をとっています。
モジュール分割を行う理由はおおむね次のようなものです。
- 可読性を維持した継続的な開発のため — モジュール間に関係を持たせることで逆の参照を防ぎ暗黙的なコードの複雑化を制御する
- ビルド速度の最適化のため — 設計次第ではありつつもビルド時における並行性の向上を狙う
アプリ全体における設計、その中にあるモジュール構成を図にすると次のようになります。
この設計に特に呼び名はありませんが、ある程度一般的と思われるプラクティスに従った責務ごとにモジュールを分割し木構造をとる依存関係です。
いわゆる「クリーンアーキテクチャ」や「レイヤードアーキテクチャ」などと呼ばれる設計にも共通する部分はあるかもしれません。
例えばAppServiceというモジュールは、UIを持たずにPairsの機能を提供するものです。 これを基盤としてAppLibraryやFeatureモジュールを初めとしてUIが実装されていきます。
機能ごとのモジュール分割 — Feature module
Pairsというサービスは目的は基本的にはひとつですが、そのために多くの機能が存在しています。
機能が増えると当然コードも増えます。
先ほどのモジュール分割はレイヤーごとの分割でしたが、機能の追加はレイヤーの中で発生します。すると特定のレイヤーが大きくなることが懸念となります。
その懸念はレイヤー分割によって制御できていた問題が制御できなくなることにあります。
特定の機能同士が改変の中で時間と共に複雑化し可読性が下がり改変が難しくなる可能性があります。
そこでレイヤーの中でも横に分割を行い依存関係が暗黙的に増加しないように制御できるようにします。
機能単位ごとに定義するモジュールのことをFeatureモジュールと呼んでいます。
しかし、この横軸におけるFeatureモジュール分割にはまた別の課題があり、それは横の依存関係は作ることが基本的にはできないことにあります。
Module A, Module Bと存在し、BがAを依存(import)にとる場合、AからBへの依存をとることはできません。ビルド時にcycle dependencyというエラーになります。
もし、画面ごとに分割を行い、さらに画面遷移が互いに出来るようにする必要が生まれた時に問題に直面します。
View Controller Aを表示したくてもそのシンボルを解決することができないのです。
そこで、横同士の関係の構築は諦め、横に存在するモジュールの存在を全て知っているレイヤーを上に配置します。
すなわち、図のような構成になります。
横の関係を構築できないということは画面遷移などの連携をFeatureモジュールは持ちません。
それらはApp側にcontainerを用意し必要な外部連携に関するアクションはFeatureモジュールから委任されます。
すなわち、Appモジュールは画面の中身の表示とその画面遷移と階層構造を管理することに集中することになります。
この構成はcoordinatorパターンと呼ばれるものに近いところはあるかもしれないですね。
🤵🏻♂️💡
この構成は比較的最近始めた取り組みの一つで、まだまだApp自体に多くの機能実装が存在しています。
InterfaceBuilder (XIB, Storyboard) は使用せず、コードでUIを実装
XIBやStoryboardなどのInterfaceBuilderは一切使用せずUIを実装しています。唯一あるのはLaunchScreenのみです。
InterfaceBuilderを使用するか否かの議論や意見は多くの場所で行われていると思いますので、私からどちらにすべきだ、という主張は行いませんが、使わない背景や利用している代替手段を説明していきます。
Texture (AsyncDisplayKit) の採用
PairsにおけるUIの多くはTextureを利用して開発されています。
Textureに関する詳細な内容についてはここでは割愛します。
TextureはUIKitをベースとし、可能な限りUI描画に必要な処理(レイアウト計算など)をバックグラウンドで処理し、メインスレッドでのタスクを減らし、デバイスが表示可能な最大のフレームレートを得ることを目的としたライブラリです。
Textureは現在はPinterestによって開発されており、Googleのいくつかのアプリでも利用されています。
Textureが持つもう一つの強力な機能はレイアウトエンジンにあります。 CSS Flexboxをベースとしたレイアウトであり、基本的にはvertical-stackとhorizontal-stackを組み合わせて作り上げることになります。
TextureのAPIをそのまま使ってレイアウトを記述するとこのようになります。
いかがでしょうか? もしかすると、「長いね」と思ったかもしれません。
ReactやSwiftUIに慣れている人なら想像がつきやすいかもしれませんが、もう少し複雑なレイアウトになったら大変になりそうな予想がつきます。
実際に大変な経験をしたので記述方法を改善するライブラリを開発し、次のように記述できるようにしました。
一見SwiftUIのような雰囲気はありますが、注意点はViewの実体を作った上で、「レイアウトはどうするか」についてのみ記述をすることにあります。
参照:
TextureをUIKitベースの既存アプリにどのように部分的に組み込むことが可能かを説明するスライド
AutoLayoutでもFlexboxのようにレイアウトを記述する — MondrianLayout
Textureは強力でとても便利なツールです。しかし、大きな依存であることは事実です。 これを使うことなくAutoLayoutとUIKitのみで実装を済ませたいケースがあることも事実です。
そこで課題に上がるのはInterfaceBuilderを使用していないことからコードでのAutoLayoutの記述を頑張る必要があることです。
そのため、AutoLayoutを簡潔にセットアップするために多くのAutoLayoutのライブラリがあります。
今までにいくつかのライブラリを使ってきましたが、(ついに?)自分で開発をしました。 そのライブラリのテーマはFlexboxもしくはSwiftUIのように記述を可能にすることです。
例外のケースはたくさんありますが、縦と横の組み合わせで表現できるレイアウトが多いのは現実だと思います。 そのようなレイアウトに対し、制約を一つづつ設定することは大変なことであり、改変することも難しくなります。
この問題を解消するために私たちがすでに持っているアプローチは UIStackView
を使うことですが、これは組み合わせの度にView(Layer)の階層が増えることが懸念でした。
🤵🏻♂️💭 余談
とはいえ、UIKitからするとそれは懸念ではないのかもしれません。
なぜなら、UIStackViewはiOS13まではbackground-colorが設定できませんでした。しかしiOS14からは設定可能になっています。 background-colorが設定できなかった理由は、描画を行わず、レイアウトを行うだけのコンテナとして最適化するためだったと推測していますが、今はbackground-colorが透明であれば描画に使うコンテキストは作成されない最適化でもあるのかもしれません。 つまり、色がなければUILayoutGuideのように動いているのかも。と考えています。
そして、私が開発したものは MondrianLayout
というもので、任意のUIViewを起点とし、そのsubviewを UILayoutGuide
を活用しながらレイアウトを行うというものです。
このように記述できることはコードでのレイアウトの実装効率を大きく向上します。
🤵🏻♂️⚠️
注意点として、こちらもTextureと同じく、レイアウトに関してのみ、宣言的に記述が出来る。というだけでUIViewのインスタンスはこれまで通り開発者がしっかりと管理する必要があります。
一方でこれではAutoLayoutが持つ本来の機能の一部を制限しているということにもなります。
例えば、階層を跨いだ制約の設定などです。これはFlexboxでは表現ができない事例でもあります。 この問題を解決するためにSwiftUIではAlignmentGuideが導入されているように思えます。
このようにUIデザインによってはFlexboxでは表現が難しい事例もありますので、Flexboxベースでなく、制約ベースでレイアウトを設定するAPIも用意しており、他のレイアウトライブラリでも見かけるような記述方法になっています。
参照:
🤵🏻♂️💭 余談
今まではEasyPeasyを使っていましたが、今回のMondrianLayoutの導入から、マイグレーションを行うためにswift-syntaxを使って半自動的に書き換えを行いました。 その時にASTを理解するのに非常に助けられたのが Swift AST Explorer でした。
Storybookを使ったUIコンポーネント開発
Storybookと聞くとWeb?と思う人もいるかもしれませんが、そうですそれのことです。
Storybookのような機能を持つライブラリをiOS用に開発を行いました。
機能実装に必要なUIコンポーネントを実際のアプリの中で行うのではなく、別のアプリなどを利用しその中でコンポーネント単体を直接開発を行う方法です。
Pairsアプリは巨大でビルドと起動に時間がかかりますし、実装するものによっては実装を確認するまでに大変なフローを経由する必要があることもしばしばあります。
Storybook-iosはライブラリとしては改善すべき部分はたくさんあるのが実情ですがPairsにおけるUIコンポーネント開発はこれによりなかなか効率良く行えています。
このStorybook-iosの実現においてもモジュール分割の構成が価値を出しています。
UIコンポーネントをPairsアプリの中に配置するのではなく、その依存モジュール側(Feature, AppLibrary, AppUIKitなど)に配置します。
そしてアプリターゲットとしてStorybook用のアプリを用意し、Pairsが依存するモジュールと同様のものをimportして起動するようにします。
参照:
Vergeによる状態管理
状態管理はFlux, Redux, MVVMなどを基に考えて開発したVergeというライブラリがアプリ全体で使用されています。 こちらについては次の記事に詳しくあります。
Compositionを基本としたUI実装
UIを実装するときに発生する問題の一つは
「UIのパターンが新しいユースケースにより、どんどん増えていくこと」
です。
こういう問題への対処方の一つとして、Design Systemの導入などが話に上がることもあります。 しかし、実装としてはあまり仕組みに頼ったものではなく実際にプログラムとして改変が行いやすく、再利用可能なコンポーネントを作り上げたいものです。
そこで、Compositionパターンを活用します。
ReactやSwiftUIなどのパラダイムでは強力な効果を持ちます。しかしPairsではUIKitでそれを行います。
UIを形成するパーツを細かくコンポーネントとして切り出して、継承をするのではなく、コンポーネントを集めて一つの集合体として見せる新たなコンポーネントとなるUIViewを定義します。 — addSubviewをするということです。
UIViewの階層構造と等しくコンポーネントも木構造を取ります。
extensionなどでスタイルを設定するコードを都度呼び出すのではなく、スタイルが適用されたコンポーネントを親コンポーネントが所有するのです。
例えば次のようなものです。
例えば「とある既存コンポーネントを新しい場所に表示することとなったが、そこでは特別な事情によりpaddingを追加する必要があった。しかし、そのコンポーネントにはpaddingを設定するオプションはない。」
この時、既存コンポーネントに実装を加えるのではなく、paddingを加えつつ表示させるコンポーネントを新しく開発し、既存資産に手を加えることなく統合を実現させます。これがcompositionパターンの強みになります。
これにより必要なコンポーネントをユースケースに合わせて詰め合わせることで柔軟に対応できます。 表現できない不都合が生じた場合は対象のコンポーネントをさらに分割します。
この手法のデメリットはUIKitにおいてはviewの階層が深くなることでパフォーマンスやリソースに負荷が増えることでしょう。
階層を増やさずに実現するべくPresenterなどを導入し、それらにコンポーネントの木構造をとってもらうことも可能ですが、ライフサイクルの管理の手間が増えます。
従ってUIViewの階層構造とライフサイクルに委ねる手法を今は選択します。
一方で、SwiftUIで開発をされている方はこのような手法を積極的に利用されているかもしれません。実際のView階層と直結せずにランタイムが最適化を行うようなパラダイムではこのような手法は非常に有効です。
アプリの起動速度 — Module分割 & static-linking
私の品質における関心事のひとつは、アプリの起動速度です。
プロセスが切られている状態でホーム画面でアプリアイコンをタップし、ユーザーが機能を使えるようになるまでの時間が短くなることが体験をよくすると考えています。
iOSアプリ開発者であれば、アプリをタスクマネージャから明確に終了させることにはあまり価値はなく、むしろそのほうが次回のアプリ起動時に多くの消費電力が発生することは知られていると思います。しかし、
私の主観ですが、ユーザーはすぐにタスクを終了させます。
履歴が残ること、スクリーンショットが残ることを避けたいと思う人や、タスクを終了させることに習慣付いてしまっているユーザーが多くを占めると私は考えています。
だからこそアプリはプロセスが終了している状態からの立ち上がりの速さが重要になると考えます。
世界のトッププレイヤーのアプリで試すと、やはり彼らのアプリの立ち上がりは非常に高速です。
ユーザーはホーム画面からアプリが目的通り操作可能になるまでの速度がしっかりとチューニングされていることがわかります。
どうしても時間がかかる場合のプレースホルダーやスケルトンUIも巧みに活用しています。
さて、アプリの起動速度に影響を与えるのは様々な要因があります。
- 特定のタイプにおけるシンボルの数
- アプリ自体の大きさ
- dynamic linkingを行うモジュールの数 (dynamic framework または dynamic library)
- 他にも色々
Emergeというアプリの分析を行うサービスがあるのですが、そこが公開している記事は非常に興味深いものばかりです。
私も過去に次のような記事を書いていますが、これはかなり実験的なやり方なので今は使用していません。
「Dynamic FrameworkをStaticにして、ひとつのDynamic Framework」というつまり、Umbrellaですが、これが必要かどうかもケース次第です。
その上位レイヤーのモジュールたちがdynamic linkingなのであれば必要になるはずです。
現在はどうしているかというと、CocoaPodsを含むすべてのframeworkとlibraryをstatic-linkingに指定しビルドしたときにアプリを実行するバイナリのみになるように設定しています。
一部の外部ライブラリがビルド済みのdynamic linkingタイプなのでそれは同梱しています。
CocoaPodsでFrameworkの形式のままstatic-linkingにする方法は次のとおりです。
use_frameworks! :linkage => :static
Firebaseのドキュメントにも近い説明があります。
🤵🏻♂️💡
use_frameworks!を指定しなければstatic-libraryになりますが、ものによってはうまくビルドできない構成になることもあるようです。
実はしばらくの間、static-linking化を運用上のコストから廃止していました。その後、今回の取り組みを実施したところ次のような結果が表れました。
Xcode Organizerで閲覧可能なLaunch Timeにて、半分はいかないものの大きな速度改善ができていることが読み取れます。
平均で1,446ms から 804msへの改善となります。
dyld3は?
iOS13からdyld3がアプリ起動時に使われるようになっています。
このことからサードパーティアプリでもアプリの起動が相当に高速化されています。
実際にPairsでもすべてがdynamic-linkingのモジュールでしたが、別にstatic対応しなくても十分に高速でした。
しかしそれは2回目以降の起動に限ります。
dyld3によって高速化される理由はキャッシュの機構を持ったことが大きな要因だからです。
つまり今回私がstatic化した理由はAppStoreからダウンロードしてからの初回起動も高速化したかったから。ということになります。
🤵🏻♂️💭
どうやらiOS15からはdyld4になっているようですが、あまりこれといった情報はないような?
SNSといえば写真 そして編集 — Brightroom
Pairsはコミュニケーションツールとしての機能も多く、ユーザーがプロフィール写真を設定したり、チャットで写真を送るなど、「写真」は重要なメディアのひとつです。
プロフィール写真を設定する
という機能はアカウント機能を持つサービスであれば、ほぼ確実に実装されるはずです。
iOSアプリでこの機能を少しでもリッチにしようとした時に大変なことになります。
iOSの標準APIを上手に使うか、OSSかSaaSか自分で実装するか。
SaaSだとPixelというものがあります。
Pairsでは私が実装したものを使っています。
お金を払ってSaaSを使うのでも良かったのですが、面白そうだったので私が夢中になって作りました。🤵🏻♂️
夢中になった結果、Pairsがビジネス上必要とするものよりも多くの機能を持ってしまったわけなのですが、今後の発展に対し価値のある投資ができたと思っています。
実際の問題として、「写真をクロップする」という機能は簡単なものに見えるかもしれません。しかし実装はそうでもないのです。
なめらかな使い心地を獲得しつつ正しく実装するには相当な知識と苦労が必要でした。
Brightroomはデザインを極力持たずにクロップするだけのコンポーネントを用意しています。これを使うことで0からエッジケースを拾い集めて開発することを避けながらプロダクトで必要なデザインを実現できます。
Sheet UI — Rideau
普及したSheet-UI, ハーフモーダル, 半モーダル
今ではかなり普及しているUIコンポーネントと言えるSheet-UI
いろんな呼び方があるみたいですが、どれが正解ですか?
画面下部からスライドインし、ケースによって高さを画面の半分程度と画面一杯まで広げることができる挙動を持つことが多いUIコンポーネントです。
iOSもいくつか前のバージョンのiOSにおける標準アプリで採用し、 iOS13からは UIViewControllerのUIModalPresentationStyleがfullScreenからpageSheetに変更となっています。
これによりSheet-UIのような画面遷移が可能になりましたが、高さを調節することは出来ませんでした。
この辺りをきっかけにOSSを多く見かけるようになりました。
私もその開発に加わった一人であり、RideauというOSSを公開し、Pairsでも利用しています。
iOS15でUISheetPresentationControllerが登場したが独自実装はまだ必要なケースはある
iOS15になりUISheetPresentationControllerが公開されています。
これで自分たち独自実装やOSSの利用が不要になるかというと、そうでない場合があると私は考えています。
それは特定の画面に常駐させて表示させたいユースケースです。
UISheetPresentationController
は UIPresentationController
のサブクラスであり、それはUIViewControllerのpresentationにおける表示方法です。
UIの見た目上、画面の半分だけを占めていたとしてもUIKitとしてはcontextが分離されています。つまり実装上は画面遷移しているということです。
一方で例えばMapsやStocks、MusicのようなSheet-UIは常に画面の下に待機しており、いつでもユーザーが引っ張り上げることができるUIを実現するには UISheetPresentationController
では実現は難しいはずです。
Sheet-UIを構えさせておき、さらにpresentationを行いたくてもpresentedViewControllerはすでに使用されているため表示はされないでしょう。
従って、この例のようなUIを実現するためには引き続き自分たちの実装を必要とします。
そもそもこれは概念が異なるものと言えるかもしれません。
UIの見た目が似ていることから実装方針を取り間違えてしまうことに注意が必要です。
加えて、presentationでのユースケースだとしても、デザインのカスタマイズやminimum deployment targetなどの制約から実装を行う必要ももちろんあります。
Pairsは?というと今のところは引き続きライブラリの利用を続けています。
- デザインを完全にカスタムできる必要がある
- iOS12以上のサポートを継続している
Rideauを利用しているUIはPairsのなかでいろんな場面で見ることができます。
- コミュニティとコミュニティチャット
- つぶやきの投稿
- いいね!の交換
🤵🏻♂️💭 余談
UIの実装は変更することが多く、自分たちのユースケースに合わせるために特別な実装になることばかりです。なので既存のOSSは直接プロダクトでは使わずに実装を行います。もちろん、OSSを実装の参考にさせてもらうことは常にあります。このことから私はエウレカで使うことを前提としつつ、ある程度一般化されたものを開発しライブラリとして公開しています。
私が公開している直接使ってもらうことも嬉しいですが、自社サービス等で直接利用することは私は強くは推奨せず、少なくとも誰かの実装の役に立てば良いと考えています。
参照:
トランジション/アニメーション
トランジション(transition)とアニメーション(animation)の違いはなんでしょうか?調べてみると色々出てきます。
transitionは遷移ですが、UIにおいてはこの中にanimationの意味も含んでいるように思えます。
遷移をアニメーションによって表現することをtransitionと呼ぶ。そんなところでしょうか。
フロントエンドではリッチな見た目を持ったtransitionが増えています。
iOSならAppStoreのTodayタブのカードから詳細へのtransitionがわかりやすい事例です。
このような見た目の効果はディスプレイ上で動作するアプリに単なるソフトウェアではなく、肌触りのようなものを与えるように私は感じます。
できることなら、至る部分で適切なtransitionを実装したいものです。
しかしながら、transitionの実装は難しすぎます。
transition — 遷移であり、from — toの繋がりを上手に表現するということになりますが、その両者ともに状態を多く持つこともあります。加えて、その遷移が必ず成功するとも限らないケースもあるでしょう。
これらを踏まえて実装をすることはあまりにも大変すぎますし、なんらかの実装ライブラリによって全てを簡単にすることも現実的ではありません。
まさに私もtransitionを包括的にサポートするライブラリを開発しましたが、求められるパターンが次々と登場してくるわけで、それを都度対応することにどれぐらい価値があるのかと疑問を持つようになりました。
結局のところ、愚直にユースケースに応じてアニメーションを書いていく方が断然効率が良いと考えるようになったのです。
しかしその中でも、小さなライブラリを活用しています。
それはコンポーネント単位での座標の変化を行うUIViewPropertyAnimatorのファクトリーです。
もう見慣れたtransitionだと思います。
compactな表示からdetailな表示へのtransitionでよく用いられる表現です。
この座標の移動の処理が多少厄介で、convertRect等を用いながらなんらかの中間表現となるUIViewをアニメーションする必要があります。
これをサポートするライブラリがこちらになります。
ふたつのUIView — fromとtoをもとに中間表現をアニメーションするPropertyAnimatorを生成します。
またAppleによるFluid User Interfaceとしてはアニメーションはinterruptibleであるべきというポイントもサポートしています。
もしユーザーがtransitionをキャンセルしようとした場合、transition中の中間表現をそのまま活用し逆のtransitionの実行に移ることができます。
コンポーネントの変化は必ずしも連続的に繋がった表現であるべき必要はなく、むしろ違和感になることもある
「A から Bへの遷移」において、全てのコンポーネントがどのように座標が変化し、見た目が変わることを表現し切ることは実装上大変ですし、違和感になることも多々あります。
実際にどう動いているか、より人間が目でみた結果、どのように見えるかが重要となります。
素早く動かすことでディテールは多少失われていてもそれらしく見えることもあります。
この例はAppleから多くの参考を見つけることができます。
例えばControl Centerで各項目はdetailを持っています。タップもしくは長押しでdetailへtransitionを行います。
この時、まるでズームインするようなスムースなアニメーションが実行されています。それぞれの要素が目線から外れないような動きをすることでユーザーはコンテキストを維持したままdetailに進んだことを理解しやすくしています。
では、このtransitionにおいて、コンポーネントが一致しているものはどれでしょうか?
外枠とラベルとAirPlayのアイコンがサイズ変更と位置調整が行われているように見えます。
それ以外は、それなりの見せ方でfade-inしています。
Musicアプリの例はどうでしょうか
実質、アートワークだけがつながっているように見えます。そして、それ以外のコンポーネントはアートワークの動きやmomentumに合わせたアニメーションを独立したコンポーネントが動いているように見えます。
このようにスローモーションで見るとわかることも多く、それを素早く動かすことで自然に見えるように設計されています。
サービス提供の終了がアナウンスされたInstagram Threadsですが、このアプリはtransition/animation共にモダンさを追求したプロダクトだったと思います。
彼らにとってThreadsはそのようなUIの実現がどこまでサービスで可能かを挑戦してみるような場所でもあったのではないかと私は考えています。もっと昔にFacebookがPaperというアプリを出したように。
今後取り組みたいこと
これまで紹介したことは2017年ごろから私とチームで開発と実装における課題解決のために少しづつ取り組みながら積み重なったものです。
その内容の量の通り、とても多くのことに取り組み、価値を生み出すことができたと実感しています。
一方で、まだまだ課題なところ、解決できていないこと、イマイチな箇所、登場する新しい技術からの新しいアイデアなど、私としてはチャレンジしてみたいことだらけです。
そのトピックは次のようなものです
- SwiftUI — 現時点では採用することの見送りの判断をしていますが、常に更新は追い、導入・活用のチャンスを見計らっています。 開発者用のメニュー等では実用しています。
- swift-concurrency — 今最もホットな技術。全てを解決するわけではないが、この技術とアイデアによって今まで実装コストから対応を見送っていたことが可能になるかもしれません。
- StoreKit Testing — まだうまく取り組めていません。IAPはサービスにとって最も大切なポイントですので、技術的に保守性を高められることに期待があります。
- もっとパフォーマンス — アプリ起動時間、フレームレート、メモリ使用量、効率の良い処理、さまざまな観点で分析を行いパフォーマンスの高いソフトウェアにしていくこと
- Emerge の活用の検討 — アプリバイナリの解析を行い、サイズや起動時間などの最適化を行うため
- KMM(Kotlin-Multi-Platfrom) — PairsはiOS, Android, Webのマルチプラットフォーム展開です。共有するものはサーバーに寄せる。という選択を取りがちですが、これからの時代 on-deviceな処理がインパクトを出していくでしょうからフロントエンドでの複雑な処理が増えることもあり得ます。その時にcross-platformな資産共有が価値を持つでしょう。
- Swift 6 — Swift 6の先駆けとして登場しているswift-concurrencyですが、version 6 となるSwiftではどのように進化をするのかとても楽しみで、そこからどんなアイデアを考えることができるでしょうか。
- ビルド時間の短縮 — 依存ライブラリも相当な数なためクリーンビルドはとても遅いです。これを改善するために何度か取り組んでいますが挫折中。 Rugbyには大きな期待があります。 私の作りかけはこちら
- トランジション・アニメーション — よりリッチなトランジションを安定した実装、そしてそれを効率よく獲得する方法
ざっと大きなトピック羅列してみましたが、プロダクトにおける機能ひとつひとつに関する課題もあります。
本記事で紹介したような、開発効率を向上させるための取り組みはAndroidチームともアイデアを交換しながら進めることもしばしばあり、Yuya KaidoがAndroidアプリ開発における記事を公開しています。