個人開発で運営している癒し系アプリで、お気に入りのハートを押したときの反応がずっと物足りませんでした。タップすると色が変わるだけ。動きがないぶん、押した実感が薄いのです。指は反応を期待しているのに、画面は静かなまま。この小さなズレが、体験全体の温度を少しだけ下げていました。
Rork Max が出力するのは本物の Swift なので、この手のUIの手触りは iOS 標準の仕組みに素直に乗せられます。SF Symbols を使っているアイコンなら、symbolEffect を数行足すだけで、押した瞬間に跳ねたり、読み込み中に脈打ったりするようになります。ライブラリの追加もアセットの差し替えも要りません。
お気に入りボタンとダウンロード中インジケータという二つの題材で、symbolEffect の使いどころを手を動かしながら並べていきます。私自身が動かしすぎて逆に見づらくなった失敗も、正直に書きます。
まず一番小さい一歩:ワンショットのbounce
最初に試したのは、お気に入りボタンを押した瞬間だけ跳ねさせる動きでした。ここで大事なのは、常時アニメーションではなく「押した回数に反応させる」という考え方です。SwiftUI では、値が変わったときにエフェクトを一度だけ再生する value: 付きのオーバーロードを使います。
import SwiftUI
struct FavoriteButton: View {
@State private var isFavorite = false
// タップ回数を数え、bounceの再生トリガーにする
@State private var tapCount = 0
var body: some View {
Button {
isFavorite.toggle()
tapCount += 1
} label: {
Image(systemName: isFavorite ? "heart.fill" : "heart")
.font(.system(size: 32))
.foregroundStyle(isFavorite ? .pink : .secondary)
// tapCountが変わるたびに一度だけ跳ねる
.symbolEffect(.bounce, value: tapCount)
}
}
}value: に渡しているのが isFavorite(Bool)ではなく tapCount(Int)である点が要点です。Bool を渡すと、同じ状態へ戻したときにエフェクトが再生されないことがあります。カウンタを別に持たせておくと、「押すたびに必ず跳ねる」が確実になります。私は最初 Bool を直接渡してしまい、二回に一回しか跳ねない挙動に数十分悩みました。
体言止めで言えば、ワンショット系エフェクトの基本は「回数で駆動する」。この一点を押さえておくと、後の応用がぶれません。
常時動かす:読み込み中のpulseとvariableColor
次はダウンロード中のインジケータです。処理が続いている間ずっと動いてほしいので、こちらは繰り返し再生になります。.pulse は明滅、.variableColor はレイヤーごとに順番に光らせるエフェクトで、電波やダウンロードのような「段階的に進む」表現に向いています。
struct DownloadingIcon: View {
let isDownloading: Bool
var body: some View {
Image(systemName: "arrow.down.circle")
.font(.system(size: 28))
.symbolEffect(
.variableColor.iterative.reversing,
isActive: isDownloading
)
}
}繰り返し系では value: ではなく isActive: を使います。isActive が true の間だけアニメーションが走り、false にすると自然に止まります。状態変数と直結できるので、ダウンロードの開始・完了に合わせて動きを切り替える処理がとても素直に書けます。
.iterative は一つずつ順に光らせる指定、.reversing は端まで行ったら折り返す指定です。この二つを外すと全レイヤーが同時に明滅し、せわしない印象になります。私は最初 .cumulative(積み上げ式)で作ってしまい、光が溜まっていく見た目がダウンロードの「減っていく」感覚と噛み合わず、作り直しました。エフェクトの選択は、見た目の好みではなく、伝えたい状態の方向と合わせるのが結局は近道でした。