SwiftUIで本のUIにチャレンジしてみた!
以前あるアプリで本型のUIのアプリを見ました
iOSエンジニアとしては、このような時にどのように実装されているかはとても気になります!
というわけで、自分で本型UIにチャレンジしてみました!
結果として自分が作れたのは以下のようなものです(リポジトリ)
まだ細かいバグはあるのですが、だいぶそれっぽいUIになりました
ちなみに写真が病院食ばかりなのは、この実装は急性虫垂炎の入院中にやったからです笑
では今回の実装で苦労したところ、工夫したところを中心に今回の実装の仕組みを紹介します
実装の仕組み
今回のUIを図にするとこんな感じです
一番上はZStack,その下に左と右のページを含むHStackがあります
その左と右のページはそれぞれZStackで構成されていて、それぞれ上からTopView, SecondView,ThirdViewを持っています(キャプチャの左側を見ると、中央のめくっているページがFirstView,その左側に見えているのがSecondView,さらにその左側に少し見えている下のページがThirdViewです)
基本的にSecondViewとThirdViewはそのページに該当する画像しているだけですが、FirstViewはページをめくった時に裏側のページも見える必要があるので、ZStackで裏側のページも見えるように2枚の画像がセットになっています
この辺りのデータはそれぞれenumで定義されており、それらを@Stateに配列でそれぞれ持たせるようにしています(コード)
この裏表に画像をZStackで表示する実装は、rotation3DEffectのmodifierを利用しています(コード)
ページをめくるアニメーションの実装
次にページをめくるUIの実装ですが、ページの回転のUIについては既述のrotation3DEffectを利用しています
左側のページは10°~170°, 右側のページは-170°~-10°の範囲で、それぞれ左側のページはページの右側を、右側のページはページの左側を、anchorとして指定し、回転の軸としています(コード)
次に指でページをめくるジェスチャーの設定ですが、これはDragGestureを使用しています
今回はそれを上記の図の一番外側のZStackに設定しています
なお今回は左側と右側のページがあるので、ジェスチャーを2つ設定していますが、そのまま.gestureのmodifierを利用するとうまくジェスチャーが動かないので、両方のジェスチャーがうまく動くように、右側のDragGestureには.simultaneousGestureのmodifierを利用しています
今回はこのページをめくるジェスチャーの程度を指定範囲内で、0~1のdoubleでanimationRatioとしてアプリ内で利用できるようにしています
あとはこのDragGestureのジェスチャーの値を上記のrotation3DEffectにbindさせれば、OKです
そしてページをめくる時のZ方向の表示順についてですが、何も設定しないと左側と右側のどちらかのページがページをめくって反対側のページに来た時に、そのページの下に表示されるようになってしまいます
それを防ぐために上記の図の③の左右それぞれのZStackに.zIndexのmodifierを利用し、現在ページをめくっているページのZStackのzIndexに1、そうでないページを0に動的に切り替え、めくっているページが必ず上に表示されるようにしています
またページをめくるのを途中でやめた時に自動でページが進むor戻る実装は、DragGestureのonEndedの時にwithAnimationを使って、animationRatioを1or0に設定しています
またwithAnimationのcompletionの中で左右のページのZStackに表示するdataSourceのenumの配列を再設定するようにしています(コード)
ページをめくった時に前後のページが自動で少しめくれるUI
今回はページをめくるジェスチャーに合わせて、左側のページの場合は左側の前のページ、右側のページの場合は右側の次のページが少しずつめくれるようになっています
これは上記のページをめくる実装の応用ですが、既述の通りTopViewについては、左側のページは10°~170°, 右側のページは-170°~-10°の範囲でrotation3DEffectで回転するようになっていますが、その下のSecondViewについては、左側のページは0°~10°, 右側のページは-10°~0°の範囲で動くようになっています
なのでSecondViewはその角度とanimationRatioをbindしています(コード)
ページをめくった時に影を表示するUI
こちらも前のセクションの実装と同じ仕組みで、各Viewに.overlay(.black.opacity(opacity))というmodifierを実装し、animationRatioに合わせて、そのopacityを動的に変更するようにしています(コード)
なおopacityも0-1の範囲にしてしまうと、影が濃くなりすぎるので、その範囲はcomputed propertyの方で調整しています(コード)
下のページが見えるUI
改めてスクショの右側を見ると、一番上の次のページが少し見えています
これはどのように実装しているかというと、実はページの幅をページのrotation3DEffectの回転の割合のanimationRatioに合わせて、小さくなるようにしてます
具体的にはそれぞれのページでページがちょうど中央にくる位置でページの幅は×0.5になるようになっており、つまりページをめくるたびに等倍のwidthから半分になるように徐々に狭くなり、中央を越えると再び等倍に向かって幅が広くなるようになっています
このためページが開かれた状態で、左側は前のページ、右側は次のページが少し見えるようになっています(幅の調整のコードはこちらです)
そして本のUI全体でスムーズに動くように、TopViewだけでなく、SecondViewも含めてページ幅の調整をしています
まとめ
今回はSwiftUIで本のUIにチャレンジしてみました!
ここに書いていない細かな調整は色々していますが、正直それを含めても、まだ挙動として怪しいところがあり、まだまだブラッシュアップする余地はたくさんあります
ここまで複雑にアニメーションや状態管理をする必要があるUIだと、かなり細かい制御が必要になります
このUIを作る上で、そもそもどのような構造にしようかというところまで考えることも大変でしたが、時々このような複雑なUIを作るのも、色々なことをキャッチアップできるし、勉強になるなと感じました
今後も今回のように色々なことに興味関心を持って、アンテナを張っていきたいと思います!