musicLineアプリ開発日記

作曲を誰でも楽しく簡単に♪

【iOS】進捗報告 8 (コミュニティ)

musicLine(iOS)についての進捗。
コミュニティについて報告します。

カテゴリーリスト
プレイヤー/詳細画面
コンテスト/ユーザーページ


はじめに

今まで作曲画面の進捗を報告していましたが、一時中断してコミュニティの開発を進めてたので今回はコミュニティの進捗です。
最近全然ブログ更新できてなかったので、今回は大ボリュームです!!画像だけでも見てってください〜😂

では、まず画面構成を確認してから実装状況を見ていきたいと思います。


画面構成

Androidアプリのスクショで画面構成を確認します。

コミュニティの3画面
コミュニティ広場
Square
ユーザーページ
UserPage
コンテスト
Contest

コミュニティ広場と命名 コミュニティと言えば、新着やランキングカテゴリー等ある画面を指しますが、設計/実装する時にはコミュニティ広場Squareとします。
これはより正確に認識を合わせるためです。
コミュニティ3画面の構成は似たような作りになっており(特にコミュニティ広場とコンテスト)、共通の処理を使い回したりします。その際に、Community表記だとコミュニティ3画面全体を指すのか、コミュニティ広場を指すのかが名前だけでは分からないのが問題になりました。そのため、Community表記だと3画面全体、Squareだとコミュニティ広場と定義し、クラスや関数名だけで関係性がわかるようにしました。


ディスプレイとコントローラー

コミュニティの3画面は似たような画面構成になっており、各画面をディスプレイDisplayとコントローラーController の領域に分けて設計します。

コミュニティ広場
Square
ユーザーページ
UserPage
コンテスト
Contest


ディスプレイ

ディスプレイDisplayは、画面上部(横持ちだと画面左部)の領域を指します。
広場・コンテストではカテゴリー説明やコンテスト内容が表示され、曲選択でソングビジュアSongVisuaに切り替わります。
ユーザーページのディスプレイはスクロールで隠れるように動作します。

ディスプレイ


コントローラー

コントローラーControllerは、画面下部(横持ちだと画面右部)の領域を指します。
3画面ともヘッダーには横一列にカテゴリータブが並び、選択できるようになっています。(カテゴリーによりヘッダーの一部が変わります)
カテゴリー選択でリストCategoryListが表示され、項目CategoryListItemをタップして曲やコンテストを選択します。
なお、コントローラーは項目の選択だけではなく、詳細画面に切り替えて内容を確認する役割もあります。

コントローラー

画面構成の意義 画面構成を考えることは複数の利点があります。
例えば、画面構成を分析することでコミュニティ3画面で似たような部分がわかり、共通化につながります。また画面構成の名前を決めることで、変数や関数名等の表記ブレを無くし、勘違いによる不具合を防ぎます。さらに画面構成を決めることで進捗報告等の認識合わせする時に、どの部分について言及しているのかが正確にわかります。
ただ、あまり正確に決めすぎると面倒くさくなるので、外枠の大事なところだけに絞る方がいいと思います。やっぱり追加実装や開発を進めていくと設計通りにはいかず、設計書の内容と異なっていきます。その違いを修正するにも労力がいるので、バランスは考えないといけないですね。


実装状況
カテゴリタイトル補足
View
Community
(共通)
UI配置
コミュニティディスプレイとコントローラーを表示。縦持ちと横持ちのレイアウト。どのディスプレイ、コントローラーを使用するかは各コミュニティ画面で派生
カテゴリーリスト群カテゴリー選択、カテゴリーリストを表示。どのカテゴリーリストを表示するかは各コミュニティ画面で派生
ページスクロールリストを表示。スワイプでリスト項目を取得。どのリスト項目を使用するかは各カテゴリーで派生
曲リスト項目各コミュニティ画面で派生系を用意する
リレー曲リスト項目左右にスワイプで前後走者の曲を表示
その他リスト項目曲ID:検索で曲ID一覧を表示、ユーザー:ユーザー検索やいいねリスト等で表示
曲詳細曲の情報と貰ったコメントを表示、送るコメントを編集する
曲の情報曲のタイトル、作者、ランキング順位、タグ、ユーザーの反応等を表示
貰ったコメントテキストとOGP特殊コメントの装飾
ソングビジュア曲再生位置の変更と可視化。曲情報でトロフィー変更。リスト/詳細ボタンで切り替え。シンプルのみ実装でリリース後に追加予定。
カテゴリー選択カテゴリータブ表示。スクロール可能。選択表示。
ユーザーアイコンユーザー画像、プレミアムリングを表示。タップでユーザーページへ
プレイヤー曲の再生、いいね・お気に入り等のボタン。現在の状況を反映再生リスト追加ボタン
設定パネルレイアウト設定、ソングビジュア設定、楽器フィルタ リリース後に実装予定
Community
Square
コミュニティ広場
UI配置
ディスプレイ状況によりカテゴリー説明/ソングビジュアを切り替え。
カテゴリー説明広場カテゴリーの説明
コントローラーカテゴリーリスト群/曲詳細を切り替え
カテゴリーリスト群Squareのカテゴリーリストを切り替えるように派生
リスト項目カテゴリー別にリスト項目を派生(ランキングなら左にランク情報)。コンテスト項目はタップでコンテスト画面へ
イベント詳細コンテストの詳細情報の表示
絞り込み検索
Community
UserPage
ユーザーページ
UI配置
ディスプレイユーザープロフィールを表示。下へスワイプした時に隠れてヘッダーが表示されるように
ユーザープロフィールユーザー情報、紹介文、アクティビティログ等表示
ユーザー情報名前、アイコン、リンクリスト、フォロー欄、紹介文表示
リンクリスト外部リンク(X等)ボタンを並べる
フォロー欄フォロー、ミュート、通知ボタン
アクティビティログ活動レベル、投稿曲数やいいね数など表示
ヘッダー
コントローラーディスプレイに追従
カテゴリーリスト群UserPageのカテゴリーリストを切り替えるように派生
リスト項目開閉するプレイリスト項目を作成。
Community
Contest
コンテスト結果
UI配置
ディスプレイ状況によりコンテスト概要/ソングビジュアを切り替え。
コンテスト概要コンテストのテーマ、内容、提案者の表示
コントローラーカテゴリーリスト群/曲詳細を切り替え
カテゴリーリスト群Contestのカテゴリーリストを切り替えるように派生
リスト項目コンテストの順位や自分の投票数を表示するリスト項目に派生
Community
ダイアログ
リストダイアログいいね・お気に入りをしたユーザーリスト表示、曲を含む再生リストの表示
メッセージアラート通知するテキストを知らせる(DLした時や削除確認など)
プロフィール編集名前、アイコン、紹介文等を編集
プレイリスト詳細プレイリストのタイトル、説明、作者、コメント等を表示
プレイリスト編集タイトル、アイコン、曲リスト等を編集
コンテスト投票開催コンテスト曲の視聴と投票
MIDI詳細曲のテンポ、音符・トラック数、使用楽器等
Commnity
Common
コミュニティ
データ構造カテゴリ等の曲情報、いいねやお気に入り等のリスポンスを保持
CommunitySongPlayerコミュニティ用にラップした曲プレイヤー
CategoryListsカテゴリーリスト群のモデル。カテゴリー別にフェッチ/キャッシュ
ListInfoカテゴリーリストのモデル、リストのスクロール位置、フェッチするページ番号等
ContextCommunity共通のCommonContextと各画面のContextがある
UserActivityManagerユーザーのアクティビティ(いいね、ミュートユーザー、再生リスト、モチーフ登録、アクセス履歴等)を管理
Service
サービスモデル
ListItemFetcherカテゴリーリスト項目のMLサーバーから取得する
MidiRepositoryMIDIファイルをサーバーから取得して貯める
UserActivityFetcherユーザーの行動をMLサーバーから取得する(再生数、再生リストに含まれているか、コメント等)
Account
Common
アカウント
データ構造ログインできる構造、アカウント情報、ログイン情報、MLユーザー情報
AccountManagerアカウントの管理、ログイン時にMLサーバーと通信
Service
サービスモデル
UserActivityRepositoryユーザーのアクティビティ(いいね、ミュートユーザー、再生リスト、モチーフ登録、アクセス履歴等)を貯める
ServerCommunicationMLサーバーと通信する処理を集約


全進捗マップを表示

カテゴリタイトル補足
View
Composition
UI配置
ガイド表示
ピアノ
スクロールエリア
小節番号
小節線
分割線拡大率によって間隔を変化
フレーズフレーズがないところはフレーズ作成ボタンを表示
ツールボタン 選択中のツールはツール色にハイライトする
メロディーライン ペンツールでの音符作成時の入力線
メロディー音符 音符の先頭にチョボをつける
リズム音符 ベース音符のみ下に表示
サンプルデータ 手入力したサンプルデータの音符を配置
連符連符は数字で表示、拡大率によって省略
ツールガイド(ペン)長さ編集しているリズム音符、伸ばした時の削る範囲音符移動時、移動先の音階をわかりやすくする
ツールガイド(指)移動中の音符影、矩形選択の枠
ツールガイド(消しゴム)削除範囲、和音のみの削除範囲
ツールガイド(フレーズ)フレーズ移動・伸縮のバー、選択範囲テキスト(iOS独自)
フレーズボタンフレーズボタンとサブボタンを配置、ツールに応じてアイコンを変更、画面拡大率に応じて縮小
フレーズタブツールに応じてアイコンを変更、拡大率に応じて縮小、スクロール状態に応じて移動
音符描画の高速化音符が多くても描画できる速さへ最適化
音符の編集状態選択や変更した時に枠線の色変更
音符音階メロディ音符に音階表示
再生バー曲を再生している時に、再生位置をバーで表示
再生ボタン曲を再生/停止を監視して、状態を変更、再生準備が遅い時はインジケーターを表示
再生速度の表示曲の再生速度を表示(iOS独自)
テスト音のガイド鳴っている音階に色をつける
Composition
ダイアログ
フレーズフレーズの作成・設定・挿入、フレーズの長さやリピート回数の設定
Community
(共通)
UI配置
コミュニティディスプレイとコントローラーを表示。縦持ちと横持ちのレイアウト。どのディスプレイ、コントローラーを使用するかは各コミュニティ画面で派生
カテゴリーリスト群カテゴリー選択、カテゴリーリストを表示。どのカテゴリーリストを表示するかは各コミュニティ画面で派生
ページスクロールリストを表示。スワイプでリスト項目を取得。どのリスト項目を使用するかは各カテゴリーで派生
曲リスト項目各コミュニティ画面で派生系を用意する
リレー曲リスト項目左右にスワイプで前後走者の曲を表示
その他リスト項目曲ID:検索で曲ID一覧を表示、ユーザー:ユーザー検索やいいねリスト等で表示
曲詳細曲の情報と貰ったコメントを表示、送るコメントを編集する
曲の情報曲のタイトル、作者、ランキング順位、タグ、ユーザーの反応等を表示
貰ったコメントテキストとOGP特殊コメントの装飾
ソングビジュア曲再生位置の変更と可視化。曲情報でトロフィー変更。リスト/詳細ボタンで切り替え。シンプルのみ実装でリリース後に追加予定。
カテゴリー選択カテゴリータブ表示。スクロール可能。選択表示。
ユーザーアイコンユーザー画像、プレミアムリングを表示。タップでユーザーページへ
プレイヤー曲の再生、いいね・お気に入り等のボタン。現在の状況を反映再生リスト追加ボタン
設定パネルレイアウト設定、ソングビジュア設定、楽器フィルタ リリース後に実装予定
Community
Square
コミュニティ広場
UI配置
ディスプレイ状況によりカテゴリー説明/ソングビジュアを切り替え。
カテゴリー説明広場カテゴリーの説明
コントローラーカテゴリーリスト群/曲詳細を切り替え
カテゴリーリスト群Squareのカテゴリーリストを切り替えるように派生
リスト項目カテゴリー別にリスト項目を派生(ランキングなら左にランク情報)。コンテスト項目はタップでコンテスト画面へ
イベント詳細コンテストの詳細情報の表示
絞り込み検索
Community
UserPage
ユーザーページ
UI配置
ディスプレイユーザープロフィールを表示。下へスワイプした時に隠れてヘッダーが表示されるように
ユーザープロフィールユーザー情報、紹介文、アクティビティログ等表示
ユーザー情報名前、アイコン、リンクリスト、フォロー欄、紹介文表示
リンクリスト外部リンク(X等)ボタンを並べる
フォロー欄フォロー、ミュート、通知ボタン
アクティビティログ活動レベル、投稿曲数やいいね数など表示
ヘッダー
コントローラーディスプレイに追従
カテゴリーリスト群UserPageのカテゴリーリストを切り替えるように派生
リスト項目開閉するプレイリスト項目を作成。
Community
Contest
コンテスト結果
UI配置
ディスプレイ状況によりコンテスト概要/ソングビジュアを切り替え。
コンテスト概要コンテストのテーマ、内容、提案者の表示
コントローラーカテゴリーリスト群/曲詳細を切り替え
カテゴリーリスト群Contestのカテゴリーリストを切り替えるように派生
リスト項目コンテストの順位や自分の投票数を表示するリスト項目に派生
Community
ダイアログ
リストダイアログいいね・お気に入りをしたユーザーリスト表示、曲を含む再生リストの表示
メッセージアラート通知するテキストを知らせる(DLした時や削除確認など)
プロフィール編集名前、アイコン、紹介文等を編集
プレイリスト詳細プレイリストのタイトル、説明、作者、コメント等を表示
プレイリスト編集タイトル、アイコン、曲リスト等を編集
コンテスト投票開催コンテスト曲の視聴と投票
MIDI詳細曲のテンポ、音符・トラック数、使用楽器等
Composition
Common
作曲共通
データ構造座標とサイズ、状態を保持する。レンダラーやコライダー、通知等のロジック
コンバーターDomainのデータ構造へ変換・逆変換
通知機構Modelに変更があった時に、Viewへ通知して再描画。変更状況を監視してキャッシュ
FingerTool
指ツール
音符の編集
データ構造フレーズの音符を取得、基点と移動ベクトルを保持して移動する
音符の移動和音や連符は上下のみに制御
音符の影移動中に操作できているかわかりやすいように(iOS独自)
内外判定音符の移動をフレーズ内に留める
衝突判定移動する音符が他の音符に重なったときの挙動
タップ選択タップした音符の子音符も選択・解除
矩形選択囲った音符を選択
音符の入れ替えリズム音符のスライドで重なった音符と入れ替える(iOS独自)
和音の削除和音を移動した時に重なる時に削除する
連符に切り替えリズム音符タップで切り替え
PenTool
ペンツール
音符の作成
データ構造音符を作成・分割・統合、フレーズへ音符を追加する
音符の作成タップで音符を作成。分割線に合わせて長さを決定
音符列の作成スワイプに沿って複数の音符を作成。音階変わる時に音符分割
有無判定タップやスワイプした縦ラインに既に音符があるか判定
音符の移動音符が既にある場合は、作成ではなく移動。一定距離進むと移動終了
音符の分割リズム音符をタップした時、音符を分割、メロディ線に沿ってY位置を動かす
音符の統合2つのリズム音符の間をタップした場合、音符を統合
音符の伸縮リズム音符を左右にスワイプすることで音符の長さを伸縮、始点で分割なし(仕様変更)
音符の消滅音符の伸縮をした時に長さが0になると削除する(iOS独自)
領域差演算音符伸縮で他の音符に重なる時は差演算して他の音符長さを削る
サンプリング補間素早くスワイプしても音符が移動できるようにサンプリング間を補間する
細かい音符分割線内の細かな音符を移動する挙動
EraserTool
消しゴムツール
音符の削除
データ構造和音と連符の認識、フレーズから音符を削除する
音符の削除タップで音符を削除
和音・連符の削除和音・連符をタップで和音・連符のみを削除(iOS独自)
音符種別判定タップした音符が和音・連符・ルート音符なのか判定する
音符列の削除スワイプで指定する削除範囲内の複数の音符を削除。
和音列の削除リズム音符をスワイプで複数の和音のみを削除。(iOS独自)
休符に切り替えリズム音符タップで休符に切り替える
PhraseTool
フレーズツール
フレーズの編集
データ構造トラックのフレーズを取得・追加、削除、状態
フレーズの作成・挿入ダイアログで長さとリピートを設定 リピートフレーズは未実装
フレーズの選択2点タップで範囲選択、範囲ガイドタップで選択解除(仕様変更)
フレーズの移動左右スワイプで移動、移動は選択フレーズとスワイプ中のフレーズを含む
フレーズの伸縮サブ編集領域をスワイプした時にフレーズ長さを伸縮、長さ0で削除(iOS独自)
フレーズの貼り付け選択フレーズをコピー・ペースト、サブボタンかフレーズタブタップで挿入、選択解除
周囲有無判定作成する小節の近くにフレーズがあるかの判定、設定できる長さやリピート回数の制御
フレーズボタン・タブツールに応じてフレーズボタン、タブの処理を変更
フレーズの結合連続する2フレーズの接する小節線をタップで結合(iOS独自)
フレーズの分割フレーズをタップで分割(iOS独自)
StampTool
スタンプツール
モチーフの編集
Transform
画面移動
座標変換
画面を上下移動ピアノのスワイプにより
画面を左右移動 スクロールエリアのスワイプにより
画面を拡大・縮小ピンチアウト・インにより基点を画面中心に設定する
画面を拡大・縮小(軸指定)長押しからのドラッグ。ピアノでX軸方向、スクロールエリアでY軸方向
MIDI コンバーターCompositionからMIDIのデータ構造へ変換
曲の再生画面左からMIDI再生、Compositionのデータ構造から曲の再生、再生位置の同期
曲の再生(最初から)再生ボタン長押しで最初から再生 最初から再生されるが、画面は最初に移動しない
曲の再生(位置指定)スクロール領域をダブルタップでその位置から再生(iOS独自)
再生速度の変更ドラッグで曲の再生速度を変更(iOS独自)
再生処理の最適化曲に変更がない時はキャッシュを使う、変更通知とキャッシュの管理
Commnity
Common
コミュニティ
データ構造カテゴリ等の曲情報、いいねやお気に入り等のリスポンスを保持
CommunitySongPlayerコミュニティ用にラップした曲プレイヤー
CategoryListsカテゴリーリスト群のモデル。カテゴリー別にフェッチ/キャッシュ
ListInfoカテゴリーリストのモデル、リストのスクロール位置、フェッチするページ番号等
ContextCommunity共通のCommonContextと各画面のContextがある
UserActivityManagerユーザーのアクティビティ(いいね、ミュートユーザー、再生リスト、モチーフ登録、アクセス履歴等)を管理
Service
サービスモデル
ListItemFetcherカテゴリーリスト項目のMLサーバーから取得する
MidiRepositoryMIDIファイルをサーバーから取得して貯める
UserActivityFetcherユーザーの行動をMLサーバーから取得する(再生数、再生リストに含まれているか、コメント等)
MIDI
Common
MIDI共通
データ構造MIDIフォーマットへ出力できる構造
コントローラー曲の読み込みや再生、再生位置の監視や音源の変更などMIDIの扱いを管理する
サンプラーピアノや音符生成時に単体のMidiイベントから音を鳴らす
Service
サービスモデル
MidiPlayerMIDIを再生する
MidiReaderMIDIファイルの読み込み
MidiWriterMIDIファイル出力
Account
Common
アカウント
データ構造ログインできる構造、アカウント情報、ログイン情報、MLユーザー情報
AccountManagerアカウントの管理、ログイン時にMLサーバーと通信
Service
サービスモデル
UserActivityRepositoryユーザーのアクティビティ(いいね、ミュートユーザー、再生リスト、モチーフ登録、アクセス履歴等)を貯める
ServerCommunicationMLサーバーと通信する処理を集約
Domain
データ構造
Json形式
Melody MelodyTrack, MelodyPhrase, NoteContainer, NoteBlock, Note
Drum DrumTrack, DrumPhrase, BeatContainer, Beat
通化 Original, Repeat, Syncの3種のPhraseをジェネリッククラスとプロトコルで抽象化
Service
サービスモデル
SongRederJsonファイルを読み込み、Domainのデータへ変換
SongWriterDomainのデータからJsonファイルへ書き出し
Common
Service
サービスモデル
リポジトリ保存データを管理

ざっくり言うとコミュニティ広場(コンテストとユーザーページ関連の部分以外)は大体の実装を完了しました。
細かい部分は色々とありますが。。あとソングビジュアや最近のAndroid追加機能等の実装はリリースしてから少しずつ機能拡充していこうと思います...


実装の詳細

コミュニティView

コミュニティViewの関係図です。

コミュニティViewの関係図

コミュニティを開いた時、エントリーはCommunityRootViewでどの画面CommunitySquareView CommunityUserPageView CommunityContestViewに遷移するかをナビゲートします。各ViewはCommunityViewをベースに派生させており、DisplayView ControllerViewがView構成要素にあります。

例えば、SquareDisplayViewはディスプレイをSquare用に派生させており、コミュニティ広場のカテゴリー説明SquareCategoryDescriptionViewを表示します。曲が選択されている時はSongVisuaViewを表示します。
SquareControllerView はディスプレイをSquare用に派生させており、選択カテゴリーSquareCategoryに応じてリストCategoryListが変わります。新着カテゴリーだと曲カテゴリーリストSongCategoryListに更新され、新着曲項目NewSongCategoryListItemが並びます。

カテゴリーリストCategoryListやカテゴリーリスト項目CategoryListItemは似たようなView構成になるので、全てを1からView構成していくのではなく、同じような部分は共通化して共通Viewを参照するようにしています。

他にも共通のViewはCommonで管理しますが、大きく分けてViewFrameViewPartsがあります。
ViewFrameは外枠をレイアウトするためのViewで、中身のViewを@ViewBuilderで渡して処理を移譲します。例えば、CategoryListsViewはヘッダーにカテゴリーセレクタCategorySelectorScrollViewがあり、選択カテゴリーに応じてリストCategoryListが表示されます。どのCategoryListを表示するかは派生したViewから処理を移譲されます。また、「横スワイプでカテゴリー変更」や「選択カテゴリータブをタップでリストを一番上まで移動」等どのCategoryListsViewでも使われる共通処理をまとめています。
ViewPartsは詳細/リストボタンやユーザーアイコン、プレイヤーなど各画面固有のViewではなく、共通で使われるViewを集約しています。


なぜ共通化をするのか? 理由はコードを改変しやすいようにするためです。
不具合修正や機能追加するとき、一箇所変更すれば良いのが理想です。同じようなコードが転々とあると、複数の箇所を変えなければならない事態になります。
「この箇所を修正する時はこの箇所も変えなければならない」そんなコードが増えると管理も大変です。そんなコードの関係性を覚えておかないと新たな不具合に繋がることもあります。
そのため、できる限り共通化してコードの再利用性を高めます。


CommunityRootView
ナビゲーション、キーボード表示、通知プレイヤー

ナビゲーションについてはNavigationStackSquare UserPage Contestのいずれかの画面遷移します。
画面遷移はCommonContextで管理して環境変数に設定するため、どのViewからでもアクセス可能です。

キーボード表示について、コメント編集時に下から迫り上がりますが、デフォルトだとレイアウトが崩れてしまいます。そのため、デフォルト動作は.ignoresSafeArea(.keyboard)でOFFにして、.onChange(of: configEnvironment.keyboardHeight)でキーボードの高さを取得して手動でオフセット.offset(y: screenOffset)しています。

public class ConfigEnvironment: ObservableObject {
    
    @Published public var keyboardHeight: CGFloat = 0
    
    private let keyboardWillShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
        .compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
        .map { $0.height }

    private let keyboardWillHide = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
        .map { _ in 0.cgf }
    
    public init(){
        keyboardWillShow.merge(with: keyboardWillHide).assign(to: &$keyboardHeight)
    }
}

あと、曲が選択された時.onChange(of: context.selectedSong)に通知プレイヤーを更新します。


CommunityView
ディスプレイ+コントローラー+プレイヤー、縦横のレイアウト

CommunityViewDisplayViewControllerViewPlayerViewをレイアウトします。
また、縦持ちと横持ちを検出して、レイアウトを変更します。

縦持ち
横持ち

DisplayViewControllerViewを各画面で派生させることで、共通ViewCommunityViewへまとめることができます。

Square派生
Contest派生
UserPage派生

プレイヤーPlayerViewは選択曲に応じて、いいねやお気に入りの数を反映する必要があります。ただし、曲の選択はControllerViewの末端で行うため、状態を共有するために環境変数CommonContextを介します。

PlayerView


DisplayView
説明ビュー / ソングビジュア、リスト詳細切り替えボタン

DisplayViewは曲選択により、説明ビューとソングビジュアが切り替わります。
また、曲選択時はリスト詳細切り替えボタンListDetailToggleButtonが表示され、ControllerViewを曲詳細SongDetailViewに切り替えることができます。

DisplayView
ListDetailToggleButton


説明ビューは各画面で派生します。

Square カテゴリーの説明
UserPage ユーザーの紹介文
Contest コンテストの説明

Squareではカテゴリーの説明

ソングビジュアは曲の再生に応じて再生バーを同期させます。
また、曲の情報から新着やランキング等のバッチを表示します。

SongVisuaView
バッチ


ContorollerView
カテゴリーリスト / 曲詳細、カテゴリー選択

ContorollerViewはリスト詳細切り替えボタンListDetailToggleButtonにより、CategoryListsViewSongDetailViewを切り替えるようになってます。

CategoryListsView
SongDetailView
CategoryListsView

CategoryListsViewはカテゴリー選択CategorySelectorScrollViewの下にカテゴリーリストCategoryListがある構成になっています。

CategorySelectorScrollView
CategoryList

どのカテゴリーを使うか、カテゴリーリストに何を表示するかを各画面で派生します。(SquareCategoryListsViewなど)


ジェネリックView
CategorySelectorScrollViewは3画面のカテゴリーに対応できるように型引数があります。
コンストラクタで渡した選択カテゴリーの型を識別してSquareCategory UserPageCategory ContestCategoryのすべてのカテゴリーを並べています。

struct CategorySelectorScrollView<Category: CommunityCategoriable>: View {

    @Binding var selected: Category

    public var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 0) {
                ForEach(Array(Category.allCases), id: \.self) { category in
                    ...
                }
            }
        ...

選択カテゴリーで型を識別


2種類のコンテキスト
コンテキストはContextCommonContextがあります。CommonContextはCommunity共通のコンテキストに対して、Contextは各画面ごとのコンテキストになります。
例えば、選択カテゴリーは各画面に状態があるため、Contextにプロパティがあります。
各画面の状態とは、「コミュニティ広場でランキング閲覧中にユーザーページへ推移して再生リストを見た」としたら、選択カテゴリーは複数の状態があるからです。

選択カテゴリー
Square ランキングカテゴリー
UserPage 再生リストカテゴリー


逆に、プレイヤーの選択曲を管理するコンテキストはCommonContextです。
ユーザーページに推移する場合も、選択曲の再生を止めずに推移したいのでコミュニティ全体で1曲の選択です。ユーザーページで曲を選択して初めて、CommonContextのプレイヤー選択曲を変更します。


CategoryList

CategoryListはリスト表示で上下にスクロールできるViewです。他にも、スクロール時にリスト項目CategoryListItemをサーバーから取得する役割があります。
これらは共通の処理でCategoryPagedScrollViewにまとめています。どのリスト項目を表示するかを各カテゴリーで派生します。

ランキング派生
コンテスト派生

また、カテゴリーによってはヘッダーに特有のリスト項目があったり、特有の機能を持たせたりします。

ヘッダーに検索欄
ユーザー検索


CategoryListItem

CategoryListItemはリストに表示される項目でカテゴリー毎など様々なViewがあります。
ただ似たようなViewもあるので、できるだけ共通化して派生します。

例えば、以下の4種類の曲リスト項目SongCategoryListItemは中身は一緒なので外枠だけ変えるように作ります。





SongCategoryListItem

他にも色々なCategoryListItemがあります。


曲ID

ユーザー

コンテスト


バッチとトロフィー
ちなみに、曲リスト項目に表示しているバッチとトロフィーは少し異なります。
トロフィーの方が小さく表示する想定なのでデザインを簡略化してます。

バッチとトロフィー

バッチ
トロフィー


SongDetailView

SongDetailViewは曲情報SongInfoViewの下に曲コメントリストSongCommentsViewがある構成になっています。

SongInfoView
SongCommentsView

さらに下側にコメントを編集して送るコメントテキストフィールドSongCommentTextFieldViewがあります。

SongCommentTextFieldView


ユーザーアイコン
プレミアムユーザーだとユーザーアイコンにプレミアムリングをつけます。

プレミアムリング

また、ユーザーアイコンの画像は毎回サーバーから取得するのでは表示が遅れてしまうため、表示した画像はNukeライブラリでキャッシュしてます。

import NukeUI

struct AccountIconView: View {
    ...
    var body: some View {
        ...
        LazyImage(iconUrl: iconUrl, transaction: .init(animation: .easeIn)) { state in
            ...
        }
        .processors([.resize(width: shortSide)])
        .priority(.high)
        .pipeline(pipeline)
    }
}

なお、Nukeライブラリでキャッシュする時の注意点ですが、ImagePipelineは同じインスタンスを使わないと適切に動作しません。
なぜかメモリが溜まり続ける不具合がありましたが、描画するたびにImagePipelineインスタンス作成していることが原因でした。
.pipeline(ImagePipeline(configuration: .withDataCache))

これでは内部のコンフィグが変わるため、同じURLでも同一画像と認識せずに新たなキャッシュが溜まり続けていました。


ListDialog

曲情報SongInfoViewのユーザーアクションボタンUserActionButtonsをタップすることでリストダイアログListDialogを表示します。

UserActionButtons

ユーザーリスト
再生リストのリスト


共通ダイアログ

ダイアログもコンテキスト同様にDialogsCommonDialogsがあります。CommonDialogsはCommunity共通、Dialogsは各画面ごとに紐づくダイアログを管理しています。
例えば、リストダイアログは各画面に状態があるため、Dialogsにアクセスしてダイアログを表示します。
「曲のいいねユーザーリストを開いて、ユーザーページに推移した」としたら、ユーザーページから曲詳細画面に戻る時には、ユーザーリストを開いた状態をキープしておいてほしいですね。

逆に「ダウンロードのメッセージを表示する」ダイアログはCommonDialogsです。
メッセージはダウンロードした時に遅延して開かれますが、「広場で曲DLして、ユーザーページに推移した」としたら、複数の画面に同じメッセージを表示する必要はないです。



おわりに

正直ちょっと一般ユーザー向けの内容じゃないですね。
内部の認識合わせにも使っているので、妙に詳細な部分とかもあります。
とはいえ公開ブログなので読んで頂いているmL民は、
δ(・ω・`)ん? もしくは ぽかーん( ゚д゚) となっているかと思います!!
ただ、とりあえずリリースに向けてiOS開発頑張っています。
そのことだけでも伝われば嬉しいです🙂

この夏リリース予定でしたが、楽しみにしていたユーザーの皆様本当に申し訳ございません!!
あと1年くらいはかかりそうです🙇
iOSでも存在感のあるアプリに仕上げようと日々磨いていますので、気長に待ってもらえると幸いです😭