I really enjoy the look of this animation running in the background of my app, but it is incredibly expensive as it stands. Is there a way to make this animation lighter on people's cpu's while maintaining its dynamic movement and changes in opacity?
To make the question simpler: How can this animation code be refactored to make it less taxing?
import SwiftUI
struct RainAnimationView: View {
var rows = 17
@State var textContent = Array("bokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokworm").map({ String($0) }).shuffled()
var randomAnimationDuration = 15.0...20.5
@State var animateRain: Bool = false
func shuffleTextContent() {
textContent.shuffle()
}
let animation = Animation.easeIn(duration: Double.random(in: 15.0...20.5)).repeatForever(autoreverses: false)
@Binding var shareVar: Bool {
didSet {
withAnimation(.easeIn(duration: Double.random(in: randomAnimationDuration)).repeatForever(autoreverses: false)) {
animateRain = true }
}
}
@State private var opacity: Double = 0.0
var body: some View {
GeometryReader { geometry in
HStack() {
Spacer()
ForEach(0..<rows, id: \.self) { index in
VStack {
Spacer()
ForEach(0..<textContent.count, id: \.self) { index in
Text(textContent[index])
.foregroundColor(Color.brown)
.font(.custom("AmericanTypewriter", size: 17))
Spacer()
}
}
.offset(y: -geometry.size.height - 100)
.offset(x: 0, y: animateRain ? 2*geometry.size.height : -geometry.size.height)
.rotation3DEffect(animateRain ? .degrees(49) : .degrees(1), axis: (x: 0, y: 1, z: 0))
.opacity(animateRain ? 1.0 : 0.0)
.animation(Animation.easeInOut(duration: Double.random(in: randomAnimationDuration)).repeatForever(autoreverses: true), value: animateRain)
}
}
.frame(width: geometry.size.width, height: geometry.size.height + 200)
.clipped()
.onAppear() { DispatchQueue.main.async { withAnimation(animation) {
animateRain = false
withAnimation(animation) {
animateRain = true
}
}}}
.ignoresSafeArea(edges: .top)
}
}
}
I tried just running it when a view loads for the first time.
Used less rows in general.
I tried running the animation for only a brief period of time.
It's still really taxing unfortunately.
If you want to know if changes are making an improvement, it is important to benchmark the performance of the solution as you currently have it.
Running on an iPhone 16 simulator with iOS 18.0, I found the main processes consuming CPU were the following (with approximate CPU values):
These values were roughly the same for both DEBUG and RELEASE builds.
One reason why the animation is expensive is because you are building each column as a VStack
containing the letters in the array, then applying the animated changes to the VStack
. So a way to reduce this cost is to replace the VStack
with a Text
view that displays all the characters in the array as a single string. Then:
In order to keep the vertical layout, a newline can be added between each of the characters.
The gaps between lines can be controlled by setting the lineSpacing
.
It is important to apply fixedSize
to each of the Text
items, or to the HStack
as a whole, otherwise the text gets truncated.
It might help to calculate the exact number of columns to be shown, based on the width of the display. This ensures that there is no overhead for columns that are off-screen.
It might also help to avoid clip operations if they are not really needed.
I couldn't really understand why you had so many state variables and why the .onAppear
was so convoluted. Maybe you are using the state variables and the function shuffleTextContent
in ways you have not shown in the example? To keep it simple, the version below uses as few state variables as possible and .onAppear
is also very basic.
struct RainAnimationView: View {
let colWidth: CGFloat = 23
let randomAnimationDuration = 15.0...20.5
let textContent = Array( "bokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokwormbokworm")
.map({ String($0) })
.shuffled()
.joined(separator: "\n")
@State private var animateRain: Bool = false
var body: some View {
GeometryReader { geometry in
let nCols = Int(geometry.size.width / colWidth)
HStack(alignment: .top, spacing: 0) {
ForEach(0..<nCols, id: \.self) { _ in
Text(textContent)
.frame(width: colWidth)
.offset(y: animateRain ? 0 : -2 * geometry.size.height)
.rotation3DEffect(.degrees(animateRain ? 49 : 1), axis: (x: 0, y: 1, z: 0))
.opacity(animateRain ? 1.0 : 0.0)
.animation(
.easeInOut(duration: Double.random(in: randomAnimationDuration))
.repeatForever(autoreverses: true),
value: animateRain
)
}
}
.fixedSize()
.foregroundStyle(.brown)
.font(.custom("AmericanTypewriter", size: 17))
.lineSpacing(10)
.onAppear { animateRain = true }
.ignoresSafeArea(edges: .top)
}
}
}
Measuring again on an iPhone 16 simulator with iOS 18.0, the CPU usage is now approximately as follows:
You will see that the CPU usage of the main app has been reduced significantly, but the other processes are using a little more CPU than before. So the optimisations certainly bring an improvement, but the animation is still quite CPU-heavy.