InstagramチームがInstagram iOS版のフィード機能をイチから作り直したとき、Collection Viewや差分の取り方、スパゲッティコードが多すぎるなど、当初考えていたよりも多くの学びがありました。このtry! Swiftの講演では、Ryan Nystromがリファクタリングが成功するために何をしたのか、オープンソースとして公開し、新しいInstagramフィードので使われているIGListKitを紹介します。
イントロダクション (0:00)
Ryan Nystromです。ニューヨークにあるInstagramのエンジニアです。インフラでとてもかっこいい仕事をしています。去年、Instagramのフィード機能を作り直していました。とても面白いプロセスで、学びも多く得られました。カンファレンスにいくと、他社プロダクトの経験に関する講演を聞くのが好きです。なぜなら、他の会社や組織での働き方を見るのは興味がそそられるからです。
フィード機能の作り直しの話に焦点を当てて、Instagramでの働き方を少し紹介します。
技術的負債 (1:29)
なぜフィード機能を作り直す必要があったのでしょう。結論から言うと、 **技術的負債 ** でした。
Instagramはリリースから6年半経っている古いアプリですが、ベースとなっているコードは変わっていません。Gitの履歴を遡って、いくつかのファイルに述べると、Instagramの初期ののコミットを見ると、その当時は、まだマニュアルでメモリ管理を行っていて、混乱していました。本当に苦しめられました。
どうやって動いているのか? (2:05)
まず、CollectionViewを使ってます。Instagramの投稿を見てみると、大きなセクションと、分割された小さな複数のセルが見えます。この内訳は以下の画像の通りです。
上部にはSupplementary Viewがあって、メディアのセル、アクションアイテムのセルがあり、画面下部までテキストのセルが並びます。これは”フィードアイテム”と呼んでいるデータモデルで動いています。
画像を見せたり、動画を再生たり、ユーザーの名前がなにか、などのコメントの個数を決めているフィードアイテムがあります。アプリ全体で、このフィードアイテムというデータモデルが動いていて、このフィードアイテムが画像、動画、コメントなどを持つことを決めています。誰かが「ねぇ、フィードにアイテムを追加したいんだけど」って言ったとして、この状況から、こう言わなければなりません。「ごめんね、できないんだ。フィードアイテムじゃないから」って。
これは誰かがデザインしたセルで、プロダクトマネージャが実装したものです。ですが、ユーザーの配列を持っていて、コメントがないので、意味がありません。チームメイトに「ダメ」って言われてました。
ただ拒否するのみ (3:41)
Instagramをリリースした2010年、Instagramには画像しかありませんでした。時を経て、動画やユーザー、他のデータモデルの並び替えを行いたい人たちが加わりました。リファクタリングや自分が正しいことをするのではなくに、ちょっとした変更をしたいと思っているいう経験がありました。それをただ拒否するのみです。
さあ、これが我々がしていることです。余分な個々の小さなモデルを含むのではなく、巨大なモデルがありました。これがとても難しくしていて、実装を遅らせるようになりました。ここで、このセクションのセルマッピングはフィードアイテムによって行われることに注意してください。 Instagramのフィードを見ているなら、1つの投稿を見ているわけではありません。あなたは束を見ています。
このデータモデルをとり、それをセクションに入れ、これらのすべてのセルを構成する何かが必要でした。それはView Controllerの責務でした。 (複数のコントローラです)Collection View用のView Controllerを持っており、ネットワークを継承したView Controllerを持ち、継承したView Controllerから一般的なフィードを継承していました。アプリのメインフィードタブには、View Controllerもありました。 4つのレベル。新たなセルを追加するのは難しかったのです。
「コードがスパゲッティになっていたんでしょう。きっと実装を遅らせるけど、それって悪いことなの?」って思うかもしれません。
ええ、かなり良くないことです。技術負債はあなたを蝕む可能性があります。本当に真剣に考える必要があると決めて、約3か月前に新しいフィードを作成しました。
Feed 2.0 (6:35)
メインゴールはView Controllerの継承関係を直すこと、フィードの複雑さを減らすこと、他のセルやデータモデルを使わせることです。私たちは、データ型に完全に無関心だった”フィードアイテム”を取り除きたいと思っていました。
Diffing (7:10)
Diffingというモデルの集合のようなものの配列をもつというコンセプトを採用しました。別の配列に移動すると、ここでstuffが削除、挿入、または移動され、値が更新されます。Diffingはインフラを構成するのにとても便利ですが、Collection Viewと組み合わせるのには大変です。
まず、古い配列のものを削除してから、リロードして何かを最終位置に移動し、最後のインデックスに基づいて挿入しなければなりません。それをうまく実行するには、ちょっとした数学が必要です。
ほとんどのDiffingのネイティブ実装は複雑です。色んな操作を行うと、とても遅くなります。私が見たほとんどの実装がバックグラウンドキューで行われていて、数学をして、フォアグラウンドに戻ってきていますが、これも遅くなる実装です。低い優先度のキューで、難しい問題を解いていました。なぜメインスレッドで行わないのでしょうか。
探してみたら、 1978年に書かれた論文 を見つけました。Paul Heckelという方が書いたものです。この論文では least common subsequence と呼ばれる手法を用いることで、この問題を線形時間で解決しています。
これによって、2つのデータの集合間でのすべての削除、更新、挿入、移動を線形時間で見つけることができるアルゴリズムを作りました。これをメインスレッドで行うので、Collection Viewのすべてのアップデートを行うことができます。モデルが単純になりました。付け加えると、Collection Viewがどのように動いているかを解明しました。あなたが想像しているよりも時間がかかっています。
アップデートを適応する (9:35)
View Controllerの問題点へと戻ります。共有オブジェクト、システム、ライブラリなどへの挿入を捨てました。View Controllerには不要です。ネットワークはネットワークで、解析のようなメインフィードはただのオブジェクトになりました。しかし、まだフィードで考慮しなければならないものがありました。
「The World」と呼んでいるコンセプトを採用していました。これはView Controllerがアイテムの配列、セクションへの入り方、適応のされ方、セルの動き方を知っているということです。これがインタラクション、ロギング、イベントの表示などに対処しています。
Item Controller (10:28)
新しくつくったインフラでは、これを分割することにしました。「item Controller」という抽象化クラスを作りました。文字通り、セクションのための小さなView Controllerです。
これはアイテムの数の決定、セルへの適応、セルのサイズの返却、インタラクションへの対処を行っています。しかし、最も大事なことは、すべてのビジネスロジックを持っていることです。空想ではありません。ただのCollection Viewですが、この方法で分割すると、Collection Viewにどんな型のオブジェクトでも追加できるのです。
新しいItem Controllerを生成して、扱うだけです。
我々も不可能だと思っていましたが、できました。チームはよろこび、新しいインフラにあわせたものを出すことができました。
何をお返しできるか? (11:26)
これを作ったあと、本当に複雑な問題を解決したことに気づいたので、コミュニティに何をお返しできるかを訪ねました。こんな複雑な問題をどうやって解決したのかを説明したいと思いました。
IGListKit (12:48)
オープンソース化した最新のフレームワークをご紹介します。IGListKit(release beta) です。
サンプルアプリとドキュメントはObjective-C nullability、アノテーション、ジェネリクスを用いて、100%Swiftで書いています。100% Swiftでコンパイルできるので、埋め込まれたC++のコードをみることはありません。
IGItemController (13:34)
フレームワークの最も重要なクラスのひとつであるIGItemController
というクラスがあります。最初に述べた「Item Controller」のコンセプトです。これはコード全てではありません。テキストラベルをもつひとつのセルです。
これを実際に使うために、item Controllerを作り、IGListItemType
プロトコルに適合させます。
class LabelItemController: IGListItemController, IGListItemType {
...
}
コンパイルするときに、numberOfItems()
など重要なメソッドを実装するようにプロトコルが警告します。
func numberOfItems() -> UInt {
return 1
}
Instagramのメインフィードには、画像の動的配列、コメント、アクションバーがあります。サイズを指定します。コンテキストオブジェクトがあることにも気をつけてください。
func sizeForItemAtIndex(index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 55)
}
このセルはスクリーンと同じ幅を持ち、高さは55ポイントです。次に、これは新しいコンセプトですが、Collection Viewとは異なります。 didUpdateToItem
です。
var item: String?
func didUpdateToItem(item: AnyObject) {
self.item = item as? String
}
これは、インフラがItem Controllerにモデルを手渡すところです。マッピングによって、これらモデルがItem Controllerに対応します。この場合、アイテムがあって、StringにOptionally Castを行い、それをインスタンス変数に格納し、cellForItemAtIndex()
でセルを使いまわします。ラベルにテキストをセットして、セルを返します。Collection ViewのData Sourceと同じですね。
Reuse Identifierのコンセプトを捨てています。そして、セルとSupplementary Viewを登録する必要性を完全に排除しました。そうです、それはItem Controllerです。しかし、インフラや何も使わないItem Controllerのいいところはなんでしょうか?
IGListAdapter (15:42)
IGListAdapter
があります。
//MARK: IgListAdapterDataSource
func itemsForListAdapter(listAdapter: IGListAdapter) -> [IGListDiffable] {
return [
"Foo",
"Bar",
"Baz"
]
}
func listAdapter(listAdapter: IGListAdapter, itemControllerForItem item: AnyObject) -> IGListItemController {
return LabelItemController()
}
これはすべてのItem ControllerとCollection Viewのデータを取得し、ひとつにまとめ、動作させます。これを使うには、Data Sourceをつなぐ必要があります。
最初のメソッドでは、配列を返すだけです。ここで、配列の中身はすべてStringです。なんでもいいです。返り値の型がIGListDiffable
となっていることに注目してください。このプロトコルに対するデフォルトの実装を提供しているので、Boxの外でも動作します。しかしながら、プロトコルをより賢いDiffingのためにオーバライドや拡張しています。今アイテムを取得する別のメソッドがあり、Item Controllerを返します。前にみたのと同じLabelItemControllerを返すだけです。
ネットワークリクエストの待機中にスピナーを追加したいとします。トークンオブジェクト(ここではスピントークンというただの NSObject
)を生成し、配列の中に追加します。これはただのプロトコルなので、入れたいモデルオブジェクトはどんなものでも並び替えられます。それから、インフラがItem Controllerについて問い合わせてきたとき、「アイテムはスピントークンですか?」とチェックします。もしそうなら、新しいSpinnerItemControllerを返します。そうでないなら、ただのラベルになるだけです。
func listAdapter(listAdapter: IGListAdapter,
itemControllerForItem item: AnyObject) -> IGListItemController {
if item === spinToken {
return SpinnerItemController()
} else {
return LabelItemController()
}
}
スピナーが、セルの間に正しく表示されています。
これだけではつまらないですが、Diffingは面白いと思います。UISearchBar
を想像してください。ユーザーがテキストを入力して変更したとき、結果を更新したいとします。
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
filterString = text
adapter.performUpdatesAnimated(true, completion: nil)
}
これがsearchBar
のデリゲートメソッドです。Stringをインスタンス変数に格納していて、performUpdatesAnimated
を呼んでします。これがインフラに新しいアイテムがあることを伝え、そのDiffをとり、Collection Viewの更新を行っています。
配列のフィルターも可能です。
let words = ["Foo", "Bar", "Baz"]
func itemsForListAdapter(listAdapter: IGListAdapter) -> [IGListDiffable] {
return words.filter { word in
return word.containsString(filterString)
}
}
Stringの配列を取り、フィルターし、その結果を返します。これはデータモデルメソッドに該当します。なぜなら、インフラに更新を伝えるときにしか呼ばれないからです。これが挿入や削除、移動などCollection Viewにおけるすべてのことを行います。Collection Viewに関しては一行も書いていません。セルやItem Controllerを適応し、アダプターに更新を伝えただけです。すべてのアニメーションや更新をBoxの外で行っています。
なぜIGListKitを使うのか (19:15)
単純なアプリがあります。Table Viewがあって、reloadData
をよびます。これが問題ですか?ええ、フィードに複数のデータ型があるなら、IGListKitを使った方がいいと思います。もしフィードが複雑になってきたら、もし僕のようにセクションの整数のEnumに対処するのに疲れたら、IGListKitがお役に立つでしょう。
早くて、クラッシュのない、アニメーションのついた更新をもつフィードが欲しければ、これが本当にうまく動きます。セルからItem Controllerまで、再利用するコンポーネントを書くようにもしてくれて、View Controllerに加わります。
1つの場所に書いたItem Controllerをドロップして、他のView Controllerやアプリの中にドロップすることができます。なぜなら、それらのコンテナには無関係だからです。また、もう私の人生でperformBatchUpdates
やreloadData
をもう一度呼び出さなくても大丈夫です。
「まあ、Instagramはこれを作ったけど、それを自分が使うべきなのか?」と思うかもしれません。まあ、約15分のスパンで、私たちはクラッシュなしで世界で390万回のDiffingを行います。すべてがメインスレッドで行われており、とんでもない速さです。
どこで使えばいいのか? (21:13)
メインフィードを書き直したかったので、このプロジェクトが始まりました。現在、これらのグリッドアイテム、アクティビティフィード、さらにはダイレクトメッセージングの複雑なセルやインタラクションを含む検索ページでは、IGListKit
が使用されています。約1か月前に立ち上げたInstagram Storiesではそれを使用しており、その小さなトレイと全画面アイテムはIGListKit
で100%構築されています。私たちはこのフレームワークを使うことに全力で取り組んでいます。それは私たちのアプリの未来です。
IGListKit coming soon
Instagramでの働き方の話を共有することで、何かを取り除き、組織内で作業しているアプリに適用できることを願っています。IGListKit
を使ったアプリが出てきたら本当にうれしいです!
About the content
2016年9月のtry! Swift NYCの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。