Xcodeのデバッグ・ブレークポイントを覚えてバグ調査が早くなったところで、次に取り組んだのが画面の見た目を良くするアニメーション実装でした。「withAnimationで囲めば何でも動く」くらいの理解で始めたのですが、想像以上にハマりどころが多かったので記録しておきます。
1. withAnimationで囲んだのに動かなかった
ボタンを押したら要素がふわっと拡大するアニメーションを作ろうとして、最初はこう書いていました。
Button("拡大") {
isExpanded = true
}
withAnimation {
// 何を囲めばいいか分からなかった
}
withAnimationのクロージャの中に何を書けばアニメーションになるのか理解しておらず、とりあえずボタンの外側に置いてみたりして全く動きませんでした。正しくは、状態を変更する代入処理そのものをwithAnimationで囲む必要があると知り、次のように直しました。
Button("拡大") {
withAnimation {
isExpanded = true
}
}
「アニメーションさせたい見た目」ではなく「アニメーションのきっかけになる状態の変更」を囲むという考え方に気づくまで、無駄に時間をかけてしまいました。
2. 意図しない要素まで一緒に動いてしまった
isExpandedの変更に応じて特定のカードだけを拡大したかったのに、画面上の他のカードまで一緒にアニメーションしてしまう現象に悩まされました。
ForEach(items) { item in
CardView(item: item)
.scaleEffect(isExpanded ? 1.2 : 1.0)
}
原因は、isExpandedという1つのBool値を全カード共通で参照していたことでした。どのカードが拡大対象なのかを区別する情報がなく、SwiftUIから見ると「全部のカードが同時に変化した」ようにしか見えていなかったのです。選択中のIDを保持する変数に変更し、各カードで自分のIDと一致するかどうかを判定する形に直したところ、狙った1枚だけが動くようになりました。
@State private var expandedID: Item.ID?
CardView(item: item)
.scaleEffect(expandedID == item.id ? 1.2 : 1.0)
3. アニメーションの種類を指定せず、動きが硬く感じた
withAnimationだけを使っていたときは、動きが常に同じ速さでカクッと終わる印象があり、なんとなく安っぽく見えていました。調べてみると、withAnimation(.default)が使われている状態で、.easeInOutや.spring()のようなアニメーションの種類を指定していなかったことが原因でした。
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
expandedID = item.id
}
.spring()に変更したところ、少し弾むような自然な動きになり、同じコード量でも見た目の印象がかなり変わることを実感しました。アニメーションは「つけるかどうか」だけでなく「どんな種類にするか」まで含めて設計するものだと分かりました。
4. リストの追加・削除アニメーションで順番が崩れた
ForEachで表示しているリストにアイテムを追加・削除する処理に.animation()をつけたところ、追加した要素がリストの真ん中に唐突に現れたり、削除時に他の要素が不自然にジャンプしたりする現象が起きました。
原因は、idに配列のインデックスを使っていたことでした。要素の並び替えや削除が起きるとインデックスがずれてしまい、SwiftUIがどの要素とどの要素が対応しているのか正しく認識できなくなっていたのです。Identifiableに準拠させて、インデックスではなく一意なIDで管理するように直したところ、追加・削除のアニメーションが自然な動きになりました。
まとめ
withAnimationは「見た目」ではなく「状態を変更する処理」を囲む- 複数の要素で1つの状態変数を共有すると、意図しない要素まで一緒にアニメーションしてしまう。IDで個別に判定する
.spring()など具体的なアニメーションの種類を指定すると、動きの印象が大きく変わる- リストのアニメーションでは、インデックスではなく一意なIDで要素を管理しないと動きが崩れる
アニメーションは「つければ見た目が良くなる」くらいに軽く考えていましたが、状態管理の設計と密接に関わっている機能だと今回の実装を通して実感しました。
次は、App Store公開に必須のプライバシーポリシーページを作った話をまとめます。
本記事は生成AI(Claude)を活用して執筆・編集しています。内容は実体験に基づき、執筆者本人が確認・監修しています。


コメント