こんにちは。フエルマネー開発者です。
前回はNavigationStackを使った画面遷移設計を行いました。
続いては、Viewに用いる共通パーツ(コンポーネント)を作成していきます。
そうです、ここに来てもまだメインのView構成はしません。周りからしっかりと固めていくのが最近の僕のスタイルです。
.animation(_:value:)とは
iOS16時点で推奨されているアニメーションのためのモディファイアです。
アニメーションさせたいViewにくっつけて使用します。
まずは以下をご覧ください。
struct ContentView: View {
@State var offset: CGFloat = 0.0
var body: some View{
VStack{
Rectangle()
.frame(width: 30, height: 30)
.offset(x: offset)
Button("ずらす"){
offset += 50
}
}
}
}
ボタンを押すたびに、正方形がoffsetされて動くコードですが、ここに先ほどのanimationをつけてみます。
struct ContentView: View {
@State var offset: CGFloat = 0.0
var body: some View{
VStack{
Rectangle()
.frame(width: 30, height: 30)
.offset(x: offset)
+ .animation(.linear(duration: 0.4), value: offset)
Button("ずらす"){
offset += 50
}
}
}
}
アニメーションが付与されて、滑らかに動くようになりました。
一行でアニメーションが作れるなんて、素晴らしい時代です。
.animation(.linear(duration: 0.4), value: offset)
このモディファイアでは、valueに入れた値の変化を監視します。今回で言うと、offsetが変化した時のViewの変化にはanimationを適用しますよという意味です。animationの種類はいくつかあります。
・linear: 一定の速度で変化する
・easeIn: 最初は遅く、だんだん早くなる
・easeOut: 最初は早く、だんだん遅くなる
・easeInOut: 遅い→早い→遅い
・default: Viewごとに決まってるらしいです
durationは、何秒かけてanimationするかです。秒単位で指定します。
実践!
それでは実際にフエルマネーで今回使用するアニメーションを作っていきましょう。
何度か使用するようなコンポーネントは、分けてどこかのファイルにまとめて置いとくのがいいと思います。全体の構成を表す大きなViewに画面遷移やらアニメーションやらonAppearやらをたくさん書くのは可読性が落ちますからね。
この記事では、フエルマネーコンポーネントの一つ、見出しテキストを紹介します。
この見出しテキストは、最初表示される時にアニメーションがつきます。
まずは完成形をご覧ください。
struct FMTitle: View{
let title: String
@State var isAppeared: Bool = false
init(_ title: String){
self.title = title
}
var body: some View{
VStack(spacing: 0){
Text(title)
.font(.title)
Rectangle()
.fill(Color.cyan.gradient)
.frame(height: 3)
}
.overlay{
GeometryReader{ g in
HStack(spacing: 0){
Rectangle()
.fill(Color.black)
.offset(x: isAppeared ? -g.size.width / 2 : 0)
Rectangle()
.fill(Color.black)
.offset(x: isAppeared ? g.size.width / 2 : 0)
}
}
}
.clipped()
.animation(.easeIn(duration: 0.6), value: isAppeared)
.onAppear(){
isAppeared.toggle()
}
}
}
意外と長いんですよね、、、
それでは部分ごとに説明していきます。
let title: String
@State var isAppeared: Bool = false
init(_ title: String){
self.title = title
}
本Viewの定義部です。titleに入れた文字列を見出しとして表示します。
isAppearedがanimationのトリガーとなる部分です。
VStack(spacing: 0){
Text(title)
.font(.title)
Rectangle()
.fill(Color.cyan.gradient)
.frame(height: 3)
}
これはアニメーション関係なく、見た目を作っているところですね。説明はあまりいらないと思います。
.overlay{
GeometryReader{ g in
HStack(spacing: 0){
Rectangle()
.fill(Color.black)
.offset(x: isAppeared ? -g.size.width / 2 : 0)
Rectangle()
.fill(Color.black)
.offset(x: isAppeared ? g.size.width / 2 : 0)
}
}
}
.clipped()
ここがアニメーションする場所です。今回は中央から見出しが見えるようにアニメーションするので、初期状態では左右からRectangleをoverlayで被せて見出しを隠します。Rectangleは背景色と同色にすることで対応しています。
今回はダークモードなのでblackにしていますが、モードに応じて自動で切り替わるようにassetで背景色を定義しておきましょう。assetでの色定義についてはまた後日、気が向いたら書きます。調べたらすぐ出ると思いますが。
ここで説明するちょいムズ要素は、以下の2つです。
・GeometryReader
・3項演算子(X ? A : B)
GeometryReader{ g in
}
GeometryReaderは、自身の位置やサイズを参照できる画期的なViewです。
引数(上記でいうgの部分)に自身の情報が格納されています。
自身の幅を取得したいときは、g.size.widthを参照すれば良いです。
ただ注意して欲しいのは、GeometryReaderはRectangleやSpacerのように、親Viewに対して満ち満ちに広がります。なので以下のようなコードを書いても、Textの大きさを認識してくれるわけではないということです。
GeometryReader{ g in
Text("hoge")
}
この場合、g.size.widthは、Text("hoge")ではなく、あくまでGeometryの親ビューによって決まるということです。
もしあなたが、Text("hoge")のサイズを取得したいのであれば、以下のコードで解決できます。
Text("hoge")
.overlay{
GeometryReader{ g in
}
}
.overlayを使えば、親ビューはText("hoge")と同じサイズになります。
今回フエルマネーの見出しでも、この手法を用いています。
続いて、3項演算子。条件演算子とも言われるみたいですが、以下の形で記述します。
[条件式] ? [値A] : [値B]
条件式がtrueの場合は値A、falseの場合は値Bを返すということです。
if-else文で分岐すると結構かさばるので、これはすごく便利です。
swiftに限らず使われる標準の記述法です。
ではこれらを理解した上でもう一度見てみましょう。
.overlay{
GeometryReader{ g in
HStack(spacing: 0){
Rectangle()
.fill(Color.black)
.offset(x: isAppeared ? -g.size.width / 2 : 0)
Rectangle()
.fill(Color.black)
.offset(x: isAppeared ? g.size.width / 2 : 0)
}
}
}
.clipped()
まとめると、isAppearedがfalse→trueになると、左のRectangleは左に動き、右のRectangleは右に動く。動く量はTextの大きさに準じて決まるということですね。
最後についているclippedですが、これはoverlayが親ビューのTextからはみ出て表示されないように、はみ出た分は切り取るということです。ビュー全体へのちょっとした配慮ですね。
.animation(.easeIn(duration: 0.6), value: isAppeared)
.onAppear(){
isAppeared.toggle()
}
そして最後、animationを実行する部分です。
isApperedが変化した時にはeaseInでアニメーションします。
.onAppear、すなわち本Viewが「表示されたとき」にisAppearedがfalse→trueになるので、animationが実行されます。
コード全体
import SwiftUI
struct ContentView: View {
@State var isPresented = false
var body: some View{
VStack{
if isPresented{
FMTitle("Fuer Money for iOS")
}
Button("appear"){
isPresented.toggle()
}
}
}
}
struct FMTitle: View{
let title: String
@State var isAppeared: Bool = false
init(_ title: String){
self.title = title
}
var body: some View{
VStack(spacing: 0){
Text(title)
.font(.title)
Rectangle()
.fill(Color.cyan.gradient)
.frame(height: 3)
}
.overlay{
GeometryReader{ g in
HStack(spacing: 0){
Rectangle()
.fill(Color.black)
.offset(x: isAppeared ? -g.size.width / 2 : 0)
Rectangle()
.fill(Color.black)
.offset(x: isAppeared ? g.size.width / 2 : 0)
}
}
}
.clipped()
.animation(.easeIn(duration: 0.6), value: isAppeared)
.onAppear(){
isAppeared.toggle()
}
}
}