💫
【SwiftUI】 回転系のアニメーション: カスタムレイアウト+Animatable bodyによるリッチなUI表現
はじめに
今回の記事では、WWDC23で紹介されていたAnimatableに準拠したViewの挙動について理解を深めるために、学習した内容を言語化していきます。
動機
WWDC23のデモで、オブジェクトが円周上を回転するアニメーションを紹介していました。
このアニメーションの実現方法を知りたくなり、実装して確めてみました。
環境
- Xcode 16.0
- Swift 6.0
実装の方針
ViewのbodyとLayoutを1フレームごとに再実行させる
(理由は後述)
- カスタムのLayout
RadialLayout
を作成 - カスタムLayout
RadialLayout
とオフセット角度offset
プロパティから成る子Viewを作成 - 親Viewで
offset
の状態を変更し、子Viewに渡す
全体のコード
親View
import SwiftUI
struct ContentView: View {
@State private var offset: Angle = .degrees(0)
var body: some View {
RadialView(offset: offset)
.padding()
.onTapGesture {
withAnimation {
offset += .degrees(180)
}
}
}
}
子View
import SwiftUI
struct RadialView: View {
var offset: Angle
var body: some View {
RadialLayout(offset: offset) {
Avatar("🐶")
Avatar("🐱")
Avatar("🐭")
}
.background {
Circle()
.strokeBorder(.gray, style: StrokeStyle(lineWidth: 4, dash: [10, 10]))
}
}
}
extension RadialView: Animatable {
var animatableData: Angle.AnimatableData {
get { offset.animatableData }
set { offset.animatableData = newValue}
}
}
struct RadialLayout: Layout {
private let offset: Angle
init(offset: Angle = Angle(degrees: 0.0)) {
self.offset = offset
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
for (index, subview) in subviews.enumerated() {
let radius = bounds.width / 3.0
let angle = (Angle.degrees(360.0 / Double(subviews.count) * Double(index)) + offset).radians
var point = CGPoint(x: 0, y: radius)
.applying(CGAffineTransform(rotationAngle: angle))
point.x += bounds.midX
point.y += bounds.midY
subview.place(at: point, anchor: .center, proposal: .unspecified)
}
}
}
ポイント
ポイントは子ViewをAnimatableに準拠させること。
これを行うことで、子ViewのbodyがAnimatable属性になり、offset
がAnimatableデータとして扱われる。
extension RadialView: Animatable {
var animatableData: Angle.AnimatableData {
get { offset.animatableData }
set { offset.animatableData = newValue}
}
}
親Viewから渡されるoffset
の変更に対して、アニメーションのフレームごとbodyが呼び出されLayoutが再実行される。
Animatableに準拠させた場合 | デフォルト |
---|---|
GitHubリポジトリへのリンク
リポジトリでも公開しているので、細かくみたい方はこちらも参考にしてみてください。
参考
WWDC23
記事
Discussion