noteのiOSアプリの新タイムラインを実装しました
YouTubeのホーム画面ってすごいですよね。
見たい動画がないとき、何回もリロードして、良さそうな動画が出てきたら見るという動きを気づいたらするようになっていました。
2022年に、noteのホームタイムラインも、自分の興味関心に応じた記事が出るようになりました。
どういう仕組みで記事をレコメンドしてるかとか機械学習をどう使ってるかとか、そういうコアな話はkihaさんの書いたこちらの記事をお読みください。
わたくしはiOSアプリの実装を担当しまして、↓アプリはこんな感じのタイムラインになりました。
本記事ではiOS側の新タイムライン実装について書こうと思います。
リリース時期的にはちょっと古い話題になりますが、個人的に今年一番大きかった案件なので、書かせてください。
Advent Calendar
この記事はnote株式会社 Advent Calendar 2022 9日目の記事です。
前回の記事はkeinumaさんの「Expoアプリの運用をいい感じに整える」でした。
新タイムラインの要件
それまでのnoteアプリのタイムラインは、シンプルな縦スクロールの画面でした。
(今もフォロー中タブを選択すれば、そちらのタイムラインが出ます)
これと比較して、新タイムラインは、
- 横スクロールが発生する
- 様々な種類のセクションが入ってくる
- セクションによってアクションが全然違う
という特徴がありました。
正確に書くと、7種類のセクションがあって、それぞれに対してレイアウトがあったり、アクションが変わったりします。
多様なインプットに対して、柔軟にUIを対応させなければいけません。
こんなときに活躍するのが、UICollectionView
のCompositional Layout
です。
もともとCompositional Layout
はnoteアプリの中で使っていたんですが、おそらく一番活躍したのではないでしょうか。
Compositional Layoutとは?
この記事でモダンCollectionViewの詳細に触れると長さ的にできないので、サラッとだけ書きます。
詳しく知りたい方はこちらの記事なんかがわかりいいと思います。
Compositional Layoutは、WWDC19で発表された、新しいCollectionViewの設定です。
かつてのTableViewやCollectionViewは、同一セルを連続して表示する、というUIでした。
しかし近年のモバイルアプリに求められるUIはデザイン要件は複雑化しています。
たとえば今のApp StoreのiOSアプリ。
横スクロールのセクションもあれば、シンプルにセルのリスト表示のセクションもあります。
このように多様なインプットに対して、柔軟にレイアウトを適用したい、というのがCompositional Layoutのモチベーションです。
まさに今回の新タイムラインの要件のためにできたような機能です。
Diffable Data Source
モダンCollectionViewのコアとなるのが、Diffable Data Sourceです。
データとレイアウトを柔軟に結びつける、というのは言葉にすると簡単ですが、
実際に実装してみると、データの更新タイミングの問題に対処するのが厄介なことに気づきます。
かつてのCollectionViewはIndexPathを使って、対象のセルを更新していました。
しかしデータとレイアウトの複雑化が進むと、更新タイミングを適切にハンドリングするのは厳しくなってきます。
そこで、データ更新をもっと簡単に差分更新して欲しい、というモチベーションから生まれたのがDiffable Data Sourceです。
note iOSアプリでは、apiからレスポンス取得したタイミングで、snapshotに対してapply()
を発行することでCollectionViewを更新してます。
この記事ではデータの扱いについての詳細は触れません。
データとレイアウトでそれぞれEnumを切る
まずAPIからのレスポンスに対して、Enumを定義しました。
enum SectionType: String, Codable, CaseIterable {
case recommendedNotes = "recommended_notes" // あなたへのおすすめ
case recommendedUsers = "recommended_users" // おすすめクリエイター
// …
}
セクションの7種類の中には、noteリストとその他のものがあります。
もしデータとレイアウトが一対一で紐づいてるなら、このまま使えばいいんですが、
データ上は何種類かあるnoteリストですが、適用したいレイアウトは一緒です。
なので、CollectionViewで使う用のEnumも定義しました。
enum Item: Hashable {
case note
case user
// …
}
横スクロールのレイアウト
Compositional Layoutの実装ですが、抜粋するとこんな感じです。
var items: [Item]?
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: collectionViewLayout)
lazy var collectionViewLayout: UICollectionViewCompositionalLayout = UICollectionViewCompositionalLayout { [weak self] index, _ -> NSCollectionLayoutSection? in
guard let item = self.items[index] else { return nil }
switch item {
case .note:
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(160), heightDimension: .estimated(196))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(160), heightDimension: .estimated(196))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
return section
}
}
}
横スクロール画面にする上で重要なのはここです。
section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
orthogonalScrollingBehavior
は、デフォルトだと.noneが指定されているので、変更します。
.continuous
にすると、セクションのサイズ分スクロールするのですが、.continuousGroupLeadingBoundary
だと、
スクロールした後の慣性が止まったときにグループのleadign位置できれいに止まってくれる、という細かい挙動の違いがありました。
タイムラインの今後の課題
noteのiOSアプリの新タイムラインの実装で何使ってるかを書きました。
タイムラインにはまだ課題がありまして、主にこの2つだと思っています。
- レコメンド精度を高める
- 横スクロールUIをなんとかする
レコメンド精度については、しがないアプリエンジニアの僕だと何もできないところなので、書けることは何もないのですが、UIについては思うところがあります。
横スクロールUIは、PC/タブレット環境であれば視認性がそれなりに高いのですが、スマホの横幅があまりない環境だと厳しいかなと思っています。
なので、モバイル向けにもっといいUIないのか、という議論を度々しています。
個人的にも、なんとか改善したいと思っています。
終わりに
次回の記事は、QAチームのkubopさんの「6ヶ月間、わたしがQAエンジニアをして悩んだこと」です。
▼noteエンジニアアドベントカレンダーはこちら
▼さらにnoteの技術記事が読みたい方はこちら
(了)
Discussion