Macro @Animatable di SwiftUI iOS 26: Bikin Animasi Custom Shape Tanpa Ribet
Pelajari cara pakai macro @Animatable di SwiftUI iOS 26 untuk membuat animasi custom shape tanpa boilerplate animatableData. Tutorial lengkap dari dasar sampai contoh lanjutan, termasuk migrasi dari cara lama.

Kalau kamu pernah bikin animasi custom shape di SwiftUI — entah itu loading indicator, grafik interaktif, atau efek visual unik — pasti sudah akrab dengan satu nama: animatableData. Property yang harus kamu implement manual setiap kali ingin SwiftUI tahu bagian mana dari shape-mu yang perlu dianimasikan.
Dan jujur aja, prosesnya lumayan menyebalkan.
Untuk satu property animatable saja, kamu butuh getter dan setter manual. Mau animasikan dua property sekaligus? Siap-siap berurusan dengan AnimatablePair. Tiga property? Nested AnimatablePair di dalam AnimatablePair. Empat? Sudah bisa bikin sakit kepala duluan. Boilerplate-nya banyak, rawan typo, dan sulit di-maintain — pokoknya nggak fun.
Nah, di iOS 26, Apple akhirnya kasih solusi yang sudah lama ditunggu: macro @Animatable. Cukup satu kata, dan semua stored property yang bisa dianimasikan langsung ditangani otomatis oleh SwiftUI. Nggak perlu animatableData, nggak perlu AnimatablePair, nggak perlu boilerplate sama sekali.
So, di artikel ini kita bakal bedah tuntas cara kerja macro ini — dari konsep dasar sampai contoh-contoh praktis yang bisa langsung kamu pakai di project. Kalau kamu sudah frustrasi dengan cara lama, percaya deh, artikel ini bakal jadi penyegaran yang menyenangkan.
Persyaratan Sebelum Mulai
Sebelum mulai coding, pastikan environment kamu sudah siap:
- Xcode 26 atau lebih baru
- iOS 26 SDK
- Deployment target minimal iOS 26.0
- Import
SwiftUIdi file Swift kamu
Macro @Animatable tersedia di semua platform Apple terbaru: iOS 26+, iPadOS 26+, macOS 26+, watchOS 26+, tvOS 26+, dan visionOS 26+. Kalau kamu masih perlu support versi lama, tenang — kamu tetap bisa pakai cara lama (animatableData) dan nanti kita bahas juga strategi migrasinya.
Cara Lama: Penderitaan dengan animatableData
Sebelum kita bahas macro baru, penting untuk paham dulu kenapa cara lama jadi masalah. Begitu kamu lihat perbandingannya, dijamin langsung ngeh bedanya.
Animasi Satu Property
Katakanlah kamu punya shape trapesium sederhana dengan satu property insetAmount yang ingin dianimasikan:
// ❌ Cara lama — manual animatableData
struct Trapezoid: Shape {
var insetAmount: Double
var animatableData: Double {
get { insetAmount }
set { insetAmount = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY))
path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.closeSubpath()
return path
}
}
Untuk satu property saja, sudah butuh 4 baris ekstra berupa computed property dengan getter/setter. Masih oke lah, masih manageable.
Animasi Dua Property — Mulai Ribet
Sekarang bayangkan kamu mau animasikan dua property sekaligus. Di sinilah drama dimulai — kamu harus pakai AnimatablePair:
// ❌ Cara lama — AnimatablePair untuk dua property
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var animatableData: AnimatablePair<Double, Double> {
get {
AnimatablePair(startAngle.radians, endAngle.radians)
}
set {
startAngle = Angle.radians(newValue.first)
endAngle = Angle.radians(newValue.second)
}
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(
center: CGPoint(x: rect.midX, y: rect.midY),
radius: rect.width / 2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: false
)
return path
}
}
Mulai kerasa kan boilerplate-nya? Dan ini baru dua property lho.
Tiga Property atau Lebih — Nightmare Mode
Untuk tiga property, kamu harus nesting AnimatablePair. Honestly, di sinilah saya pribadi mulai merasa cara ini nggak sustainable:
// ❌ Cara lama — nested AnimatablePair (tiga property)
struct Wave: Shape {
var amplitude: Double
var frequency: Double
var phase: Double
var animatableData: AnimatablePair<Double, AnimatablePair<Double, Double>> {
get {
AnimatablePair(amplitude, AnimatablePair(frequency, phase))
}
set {
amplitude = newValue.first
frequency = newValue.second.first
phase = newValue.second.second
}
}
func path(in rect: CGRect) -> Path {
// ... implementasi wave
var path = Path()
for x in stride(from: 0, through: rect.width, by: 1) {
let relativeX = x / rect.width
let angle = frequency * relativeX * 2 * .pi + phase
let y = sin(angle) * amplitude + rect.midY
if x == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
return path
}
}
Perhatikan newValue.second.first dan newValue.second.second. Kalau ada empat property, kamu bakal punya newValue.second.second.first. Ini bukan kode yang enak dibaca, apalagi kalau harus di-debug jam 2 pagi pas deadline.
Cara Baru: Macro @Animatable di iOS 26
Oke, sekarang bagian yang ditunggu-tunggu. Mari kita lihat bagaimana macro @Animatable mengubah semua kode tadi jadi jauh lebih sederhana.
Konsep Dasar
Macro @Animatable adalah member dan extension macro yang ketika diterapkan pada sebuah struct, class, atau enum, otomatis men-synthesize conformance ke protocol Animatable beserta semua requirement-nya. Macro ini melihat semua stored property yang conform ke VectorArithmetic, lalu menggabungkannya menjadi satu animatableData yang di-generate secara otomatis.
Artinya? Kamu cukup tulis @Animatable di atas deklarasi type-mu, dan selesai. Sesimpel itu.
Contoh Pertama: Trapesium yang Lebih Bersih
// ✅ Cara baru — @Animatable macro
@Animatable
struct Trapezoid: Shape {
var insetAmount: Double
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY))
path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.closeSubpath()
return path
}
}
Bandingkan dengan versi lama. Nggak ada animatableData, nggak ada getter/setter. Macro @Animatable otomatis mendeteksi bahwa insetAmount bertipe Double (yang conform ke VectorArithmetic) dan mengurus semuanya di belakang layar. Bersih banget.
Contoh Kedua: Arc dengan Dua Property Animatable
@Animatable
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
@AnimatableIgnored var clockwise: Bool
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(
center: CGPoint(x: rect.midX, y: rect.midY),
radius: rect.width / 2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: clockwise
)
return path
}
}
Perhatikan dua hal penting di sini:
- Dua property animatable (
startAngledanendAngle) ditangani otomatis — tanpaAnimatablePairsama sekali. @AnimatableIgnoreddipakai untukclockwisekarenaBoolmemang nggak seharusnya dianimasikan (dan nggak conform keVectorArithmeticjuga).
Contoh Ketiga: Wave dengan Tiga Property — Tanpa Nested Pair
@Animatable
struct Wave: Shape {
var amplitude: Double
var frequency: Double
var phase: Double
func path(in rect: CGRect) -> Path {
var path = Path()
for x in stride(from: 0, through: rect.width, by: 1) {
let relativeX = x / rect.width
let angle = frequency * relativeX * 2 * .pi + phase
let y = sin(angle) * amplitude + rect.midY
if x == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
return path
}
}
Ini shape yang sama persis dengan versi lama di atas — tapi tanpa 10 baris boilerplate animatableData. Tiga property, semua animatable, tanpa AnimatablePair nested sedikitpun. Kalau ini nggak bikin kamu senyum, saya nggak tahu lagi apa yang bisa.
Memahami @AnimatableIgnored
Karena macro @Animatable otomatis menganimasikan semua stored property yang conform ke VectorArithmetic, kadang kamu perlu cara untuk mengecualikan property tertentu. Di sinilah @AnimatableIgnored berperan — dan ini penting banget untuk dipahami.
Kapan Harus Pakai @AnimatableIgnored?
Gunakan @AnimatableIgnored ketika:
- Property bertipe yang nggak bisa dianimasikan secara logis (seperti
Bool,String,Color) - Property bertipe numeric tapi kamu nggak ingin dia ikut berubah saat animasi berjalan
- Property bersifat konfigurasi statis — misalnya jumlah segmen, warna, atau mode tertentu yang nggak perlu transisi smooth
Contoh Praktis: Polygon Shape
@Animatable
struct Polygon: Shape {
var sides: Double
var scale: Double
@AnimatableIgnored var rotation: Angle
func path(in rect: CGRect) -> Path {
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2 * scale
let actualSides = max(Int(sides), 3)
var path = Path()
for i in 0..<actualSides {
let angle = (Double(i) / Double(actualSides)) * 2 * .pi
- .pi / 2 + rotation.radians
let point = CGPoint(
x: center.x + radius * cos(angle),
y: center.y + radius * sin(angle)
)
if i == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.closeSubpath()
return path
}
}
Di sini, sides dan scale bakal dianimasikan dengan smooth transition saat berubah. Tapi rotation ditandai @AnimatableIgnored, jadi perubahan rotasi terjadi instan tanpa interpolasi. Ini berguna kalau kamu mau rotasi langsung loncat ke posisi baru tanpa efek transisi — tergantung use case-mu tentunya.
Menggunakan Shape Animatable di View
Teori sudah cukup, sekarang mari kita lihat bagaimana menggunakan shape-shape ini di dalam view yang nyata. Berikut beberapa contoh interaktif yang bisa langsung kamu coba.
Demo 1: Interactive Arc
struct ArcDemoView: View {
@State private var startAngle: Angle = .degrees(0)
@State private var endAngle: Angle = .degrees(180)
var body: some View {
VStack(spacing: 32) {
Arc(
startAngle: startAngle,
endAngle: endAngle,
clockwise: false
)
.stroke(
LinearGradient(
colors: [.blue, .purple],
startPoint: .leading,
endPoint: .trailing
),
style: StrokeStyle(lineWidth: 8, lineCap: .round)
)
.frame(width: 200, height: 200)
Button("Animasikan Arc") {
withAnimation(.easeInOut(duration: 1.0)) {
startAngle = .degrees(Double.random(in: 0...180))
endAngle = .degrees(Double.random(in: 180...360))
}
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
Setiap kali tombol ditekan, startAngle dan endAngle berubah ke nilai random dengan animasi yang smooth. Karena pakai @Animatable, SwiftUI tahu kedua property ini harus diinterpolasi — tanpa kamu menulis satu baris animatableData pun. Coba bayangkan berapa baris kode yang kamu hemat di sini.
Demo 2: Animated Wave
struct WaveDemoView: View {
@State private var phase: Double = 0
@State private var amplitude: Double = 30
@State private var frequency: Double = 3
var body: some View {
VStack(spacing: 24) {
Wave(
amplitude: amplitude,
frequency: frequency,
phase: phase
)
.stroke(.cyan, lineWidth: 3)
.frame(height: 150)
.drawingGroup()
HStack(spacing: 16) {
Button("Gelombang Rendah") {
withAnimation(.easeInOut(duration: 0.8)) {
amplitude = 15
frequency = 2
}
}
Button("Gelombang Tinggi") {
withAnimation(.easeInOut(duration: 0.8)) {
amplitude = 50
frequency = 5
}
}
}
.buttonStyle(.bordered)
}
.padding()
.onAppear {
withAnimation(
.linear(duration: 2)
.repeatForever(autoreverses: false)
) {
phase = .pi * 2
}
}
}
}
Contoh ini menunjukkan sesuatu yang menurut saya keren — animasi continuous (gelombang berjalan terus-menerus) yang dikombinasikan dengan animasi interaktif di mana user bisa mengubah amplitude dan frequency secara real-time. Perhatikan juga penggunaan .drawingGroup() — modifier ini mengaktifkan rendering via Metal, yang sangat membantu performa saat shape-mu melibatkan banyak kalkulasi path.
Demo 3: Polygon Morphing
struct PolygonDemoView: View {
@State private var sides: Double = 3
@State private var scale: Double = 1.0
var body: some View {
VStack(spacing: 32) {
Polygon(
sides: sides,
scale: scale,
rotation: .degrees(0)
)
.fill(
AngularGradient(
colors: [.red, .orange, .yellow, .green, .blue, .purple, .red],
center: .center
)
)
.frame(width: 200, height: 200)
VStack(spacing: 12) {
Text("Sisi: \(Int(sides))")
.font(.headline)
Slider(value: $sides, in: 3...12, step: 1)
.onChange(of: sides) { oldValue, newValue in
withAnimation(.spring(duration: 0.6, bounce: 0.3)) {
// sides sudah diupdate oleh binding
}
}
}
Button("Pulse") {
withAnimation(.easeInOut(duration: 0.3)) {
scale = 0.8
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation(.spring(duration: 0.5, bounce: 0.5)) {
scale = 1.0
}
}
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
Polygon morphing dari segitiga ke lingkaran (semakin banyak sisi, semakin mendekati lingkaran) adalah salah satu visual yang paling satisfying untuk dianimasikan. Dulu implementasinya bikin pusing dengan AnimatablePair — sekarang? Tinggal tempel @Animatable dan beres.
@Animatable untuk ViewModifier — Bukan Cuma Shape
Satu hal yang sering terlewat (dan menurut saya ini underrated): macro @Animatable nggak cuma untuk Shape. Kamu juga bisa memakainya untuk custom ViewModifier. Ini sangat berguna untuk bikin efek transisi kustom.
@Animatable
struct ShakeModifier: ViewModifier {
var progress: Double
@AnimatableIgnored var intensity: Double
func body(content: Content) -> some View {
content
.offset(x: sin(progress * .pi * 6) * intensity * (1 - progress))
}
}
extension View {
func shake(progress: Double, intensity: Double = 10) -> some View {
modifier(ShakeModifier(progress: progress, intensity: intensity))
}
}
// Penggunaan:
struct ShakeDemoView: View {
@State private var shakeProgress: Double = 0
var body: some View {
VStack(spacing: 24) {
Text("Goyangkan Aku!")
.font(.title2.bold())
.shake(progress: shakeProgress, intensity: 15)
Button("Shake!") {
shakeProgress = 0
withAnimation(.easeOut(duration: 0.6)) {
shakeProgress = 1
}
}
.buttonStyle(.borderedProminent)
}
}
}
Modifier ini menganimasikan progress dari 0 ke 1, menghasilkan efek getaran yang perlahan mereda. Property intensity ditandai @AnimatableIgnored karena kita nggak mau nilai kekuatan getarannya ikut berubah selama animasi — dia cuma konfigurasi statis yang menentukan seberapa kuat goyangannya.
Contoh Lanjutan: Loading Indicator Custom
Nah, sekarang mari kita gabungkan semua konsep yang sudah dipelajari untuk membuat sesuatu yang lebih ambisius — custom loading indicator berbentuk petal flower yang berputar. Ini contoh favorit saya karena menunjukkan betapa bersihnya kode animasi di iOS 26.
@Animatable
struct PetalShape: Shape {
var petalCount: Double
var innerRadius: Double
var outerRadius: Double
@AnimatableIgnored var rotationOffset: Double
func path(in rect: CGRect) -> Path {
let center = CGPoint(x: rect.midX, y: rect.midY)
let count = max(Int(petalCount), 2)
var path = Path()
for i in 0..<count {
let baseAngle = (Double(i) / Double(count)) * 2 * .pi + rotationOffset
let tipAngle = baseAngle
let leftAngle = baseAngle - .pi / Double(count)
let rightAngle = baseAngle + .pi / Double(count)
let tipPoint = CGPoint(
x: center.x + outerRadius * cos(tipAngle),
y: center.y + outerRadius * sin(tipAngle)
)
let leftControl = CGPoint(
x: center.x + innerRadius * cos(leftAngle),
y: center.y + innerRadius * sin(leftAngle)
)
let rightControl = CGPoint(
x: center.x + innerRadius * cos(rightAngle),
y: center.y + innerRadius * sin(rightAngle)
)
path.move(to: center)
path.addQuadCurve(to: tipPoint, control: leftControl)
path.addQuadCurve(to: center, control: rightControl)
}
return path
}
}
struct FlowerLoadingView: View {
@State private var petalCount: Double = 6
@State private var rotationOffset: Double = 0
@State private var outerRadius: Double = 60
var body: some View {
VStack(spacing: 32) {
PetalShape(
petalCount: petalCount,
innerRadius: 30,
outerRadius: outerRadius,
rotationOffset: rotationOffset
)
.fill(.linearGradient(
colors: [.pink, .purple, .indigo],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
.frame(width: 200, height: 200)
.rotationEffect(.radians(rotationOffset))
HStack(spacing: 12) {
Button("3 Kelopak") {
withAnimation(.spring(duration: 0.8)) {
petalCount = 3
outerRadius = 80
}
}
Button("6 Kelopak") {
withAnimation(.spring(duration: 0.8)) {
petalCount = 6
outerRadius = 60
}
}
Button("12 Kelopak") {
withAnimation(.spring(duration: 0.8)) {
petalCount = 12
outerRadius = 50
}
}
}
.buttonStyle(.bordered)
}
.onAppear {
withAnimation(
.linear(duration: 4)
.repeatForever(autoreverses: false)
) {
rotationOffset = .pi * 2
}
}
}
}
Di contoh ini, petalCount, innerRadius, dan outerRadius semua dianimasikan secara otomatis oleh macro. Sementara rotationOffset di-handle oleh .rotationEffect pada level view (itulah kenapa ditandai @AnimatableIgnored di level shape). Bayangkan menulis semua ini dengan nested AnimatablePair — bisa dua kali lebih panjang kodenya dan tiga kali lebih pusing debugnya.
Tipe yang Didukung dan Tidak Didukung
Macro @Animatable bekerja dengan property yang conform ke protocol VectorArithmetic. Berikut daftar tipe yang umum dipakai supaya kamu nggak perlu trial-and-error:
Didukung (Otomatis Animatable)
Double,Float,CGFloatAngle(karenaAngle.AnimatableDataadalahDouble)CGSize,CGPoint,CGRectUnitPointAnimatablePair(kalau kamu bikin wrapper custom)- Custom type yang sudah conform ke
VectorArithmetic
Tidak Didukung (Harus Pakai @AnimatableIgnored)
Int,UInt, dan semua integer type — gunakanDoublelalu konversi keIntsaat pakaiBoolStringColorArray,Dictionary, dan collection lainnya- Enum (kecuali kamu bikin conformance
VectorArithmeticsendiri — tapi biasanya nggak worth it)
Tips penting: Kalau kamu butuh property bertipe Int yang animatable (misalnya jumlah sisi polygon), simpan sebagai Double dan konversi ke Int di dalam path(in:). Ini trik klasik yang tetap berlaku di era macro — dan honestly, ini cara paling clean untuk menangani kasus ini.
Strategi Migrasi dari animatableData ke @Animatable
Kalau kamu punya codebase yang sudah pakai animatableData dan mau migrasi, kabar baiknya prosesnya cukup straightforward. Berikut langkah-langkahnya:
Langkah 1: Tambahkan @Animatable
Taruh macro @Animatable di atas deklarasi struct:
@Animatable // tambahkan ini
struct MyCustomShape: Shape {
// ...
}
Langkah 2: Hapus animatableData
Hapus seluruh computed property animatableData beserta getter dan setter-nya. Rasanya lega banget ngehapus kode boilerplate ini, percaya deh.
Langkah 3: Tandai Property Non-Animatable
Tambahkan @AnimatableIgnored pada property yang nggak seharusnya dianimasikan:
@AnimatableIgnored var showGrid: Bool
@AnimatableIgnored var label: String
Langkah 4: Pakai Conditional Compilation untuk Backward Compatibility
Kalau kamu masih perlu support iOS versi lama, ada dua pendekatan. Pertama, pisah file:
// File: WaveShape_iOS26.swift (target iOS 26+)
@Animatable
struct Wave: Shape {
var amplitude: Double
var frequency: Double
var phase: Double
func path(in rect: CGRect) -> Path {
// implementasi sama
}
}
// File: WaveShape_Legacy.swift (target iOS 18-25)
struct WaveLegacy: Shape {
var amplitude: Double
var frequency: Double
var phase: Double
var animatableData: AnimatablePair<Double, AnimatablePair<Double, Double>> {
get { AnimatablePair(amplitude, AnimatablePair(frequency, phase)) }
set {
amplitude = newValue.first
frequency = newValue.second.first
phase = newValue.second.second
}
}
func path(in rect: CGRect) -> Path {
// implementasi sama
}
}
Atau kalau kamu lebih suka semuanya dalam satu file (yang menurut saya lebih practical untuk kebanyakan project), pakai conditional compilation:
#if canImport(SwiftUI) && compiler(>=6.2)
@Animatable
struct Wave: Shape {
var amplitude: Double
var frequency: Double
var phase: Double
func path(in rect: CGRect) -> Path {
wavePathImpl(in: rect, amplitude: amplitude,
frequency: frequency, phase: phase)
}
}
#else
struct Wave: Shape {
var amplitude: Double
var frequency: Double
var phase: Double
var animatableData: AnimatablePair<Double, AnimatablePair<Double, Double>> {
get { AnimatablePair(amplitude, AnimatablePair(frequency, phase)) }
set {
amplitude = newValue.first
frequency = newValue.second.first
phase = newValue.second.second
}
}
func path(in rect: CGRect) -> Path {
wavePathImpl(in: rect, amplitude: amplitude,
frequency: frequency, phase: phase)
}
}
#endif
// Shared path implementation
func wavePathImpl(in rect: CGRect, amplitude: Double,
frequency: Double, phase: Double) -> Path {
var path = Path()
for x in stride(from: 0, through: rect.width, by: 1) {
let relativeX = x / rect.width
let angle = frequency * relativeX * 2 * .pi + phase
let y = sin(angle) * amplitude + rect.midY
if x == 0 { path.move(to: CGPoint(x: x, y: y)) }
else { path.addLine(to: CGPoint(x: x, y: y)) }
}
return path
}
Tips Performa untuk Animasi Shape
Animasi shape bisa jadi berat kalau nggak hati-hati. Berikut beberapa tips supaya animasimu tetap smooth di semua device:
1. Gunakan .drawingGroup() untuk Shape Kompleks
Modifier ini mengalihkan rendering ke Metal GPU, yang jauh lebih cepat untuk path yang kompleks:
Wave(amplitude: 30, frequency: 5, phase: phase)
.stroke(.blue, lineWidth: 2)
.drawingGroup() // render via Metal
2. Batasi Jumlah Titik Path
Kalau kamu pakai stride untuk generate path (seperti contoh wave di atas), jangan pakai step terlalu kecil. Step 1 pixel biasanya sudah cukup smooth — nggak perlu 0.5 atau 0.1. Overkill dan cuma bikin GPU kerja ekstra tanpa perbedaan visual yang berarti.
3. Hindari Animasi Terlalu Banyak Property Sekaligus
Meskipun macro @Animatable memudahkan animasi banyak property, bukan berarti kamu harus menganimasikan semuanya sekaligus. Pilih property yang memberikan dampak visual paling besar — kadang less is more.
4. Gunakan Spring Animation untuk Interaksi
Spring animation memberikan feel yang lebih natural dibanding linear atau easeInOut untuk interaksi user. Ini salah satu detail kecil yang bikin app terasa polished:
withAnimation(.spring(duration: 0.6, bounce: 0.3)) {
sides = 8
}
FAQ: Pertanyaan yang Sering Ditanyakan
Apakah macro @Animatable bisa dipakai di iOS versi lama?
Sayangnya tidak. Macro @Animatable membutuhkan iOS 26.0 atau lebih baru. Untuk versi iOS sebelumnya, kamu tetap harus menggunakan animatableData secara manual. Tapi kamu bisa menggunakan conditional compilation (#if compiler(>=6.2)) untuk mendukung kedua pendekatan dalam satu codebase — seperti yang sudah kita bahas di bagian migrasi.
Apa bedanya @Animatable dengan protocol Animatable yang lama?
Protocol Animatable masih ada dan masih dipakai di belakang layar. Macro @Animatable hanya otomatis men-generate conformance ke protocol tersebut beserta implementasi animatableData-nya. Jadi macro ini bukan pengganti protocol — dia adalah shortcut yang menghilangkan boilerplate. Di balik layar, kode yang dihasilkan sama persis.
Kenapa property Int tidak bisa langsung dianimasikan?
Tipe Int tidak conform ke protocol VectorArithmetic karena integer tidak mendukung interpolasi pecahan. Coba bayangkan: bagaimana kamu mau interpolasi dari 3 ke 6 secara smooth kalau nggak boleh pakai nilai desimal? Solusinya, gunakan Double sebagai tipe stored property, lalu konversi ke Int di dalam method path(in:). Dengan begitu, SwiftUI bisa menginterpolasi nilai secara smooth (3.0, 3.2, 3.5, ..., 6.0).
Apakah @Animatable juga berfungsi untuk ViewModifier?
Ya, dan ini salah satu bagian terbaik dari fitur ini. Macro @Animatable bekerja untuk Shape, ViewModifier, dan type lain yang membutuhkan conformance ke protocol Animatable. Sangat berguna untuk membuat efek transisi kustom seperti shake, fade, atau slide.
Apakah ada dampak performa saat menggunakan macro @Animatable?
Nggak ada. Zero overhead. Macro @Animatable hanya menghasilkan kode yang sama persis dengan yang sebelumnya harus kamu tulis manual — yaitu implementasi animatableData. Ini murni compile-time code generation, tanpa overhead runtime tambahan apapun. Jadi kamu bisa pakai tanpa khawatir soal performa.


