Issue #68
โ Part 4 of Start building with Rive for iOS
Welcome to issue #68 of the iOS Coffee Break Newsletter ๐ฌ.
Last week, we carried on with our "Start building with Rive for iOS" series and wrapped up part 3.
If you haven't had a chance to read it yet, I recommend checking it out first to get the most value from the series.
Now, we're moving on to part 4 and last edition of the series, where we'll explore topics like creating advanced workflows with Rive Scripting and improving the animation logic using the AI coding agent.
The Plan
In this edition, we'll put together a particle system using the new Scripting feature and produce a fully dynamic falling-particle effect. To wrap things up, we'll export the Rive animation and run it with the Rive runtime on iOS.
If you haven't worked with Scripting before, I recommend checking out Rive's official Scripting videos before continuing.
Creating Artboards
To start, we'll create an artboard for a single particle in the Rive editor.
I'll set its dimensions to 200ร200 pixels and name it Particle.
Then, I'll upload an image of my newsletter sticker and place it onto the artboard.
After that, I'll align it to the center, scale it so it fills the artboard, and turn the artboard into a component so it can be reused later.
At this point, we have a reusable particle component. Here's what it looks like:
Next, we'll add the main artboard, which will be used to build the actual particle effect.
Let's create a new artboard sized at 850ร1650 pixels and name it CoffeeParticles.
We'll also turn this artboard into a component.
Here's the result:
Finally, we'll create a script capable of generating these particles dynamically.
Creating Scripts
To add a script, open the toolbox and select a type of script.
I'll choose a Node Script for this setup.
Node scripts allow you to draw shapes, images, text, artboards, and more.
Rive automatically provides some starter code, which we'll modify and reuse to build our particle system.
-- Define the script's data and inputs.
type MyNode = {}
-- Called once when the script initializes.
function init(self: MyNode): boolean
return true
end
-- Called every frame to advance the simulation.
-- 'seconds' is the elapsed time since the previous frame.
function advance(self: MyNode, seconds: number): boolean
return false
end
-- Called when any input value changes.
function update(self: MyNode) end
-- Called every frame (after advance) to render the content.
function draw(self: MyNode, renderer: Renderer) end
-- Return a factory function that Rive uses to build the Node instance.
return function(): Node<MyNode>
return {
init = init,
advance = advance,
update = update,
draw = draw,
}
endPerfect! Next, let's hook up the particle artboard to the script.
Connecting the Artboard to the Script
Right now, the script doesn't have access to any artboards, so we'll need to set that up. To do this, we'll add an artboard input to the script and connect it directly to the particle artboard.
In the inputs section of the script, add a new artboard input.
Set its property to Input and specify the type as Artboard.
-- Define the script's data and inputs.
type MyNode = {
particle: Input<Artboard>,
}
[...]If the syntax looks unfamiliar, Rive's official scripting tutorials cover this in detail.
Every input we declare must also be initialized.
Scroll down to the code section where all variables are initialized and add this new input there as well.
Here, we'll reference the particle input and set its value to late, which tells Rive that the value will be supplied from outside the script rather than defined internally.
[...]
-- Return a factory function that Rive uses to build the Node instance.
return function(): Node<MyNode>
return {
init = init,
advance = advance,
update = update,
draw = draw,
particle = late(),
}
endAfter saving the script, a new artboard input will appear in the script's properties panel, ready to be assigned.
When you open it, you'll be able to select the Particle component.
The next step is to use the draw and advance functions to render the script on screen. The advance function runs every frame and updates the animation logic while draw runs after that and renders the element to the screen.
So, let's use self to access the particle input we created and then call the draw function on it.
[...]
-- Called every frame (after advance) to render the content.
function draw(self: MyNode, renderer: Renderer)
self.particle:draw(renderer)
end
[...]After this, we'll use the AI agent to handle the code and the more complex logic.
Next, let's update the advance function. We'll use self again to access the particle input, call its advance function and pass in the seconds value.
[...]
-- Called every frame to advance the simulation.
-- 'seconds' is the elapsed time since the previous frame.
function advance(self: MyNode, seconds: number): boolean
self.particle:advance(seconds)
return false
end
[...]Once saved, we should now see the particle on screen.
Setting the Artboard's width and height
Next, we want the particle to travel from the top of the artboard all the way to the bottom. To make this happen, the script needs to be aware of the artboard's dimensions.
So, we'll add two numeric inputs to represent the artboard's width and height.
-- Define the script's data and inputs.
type MyNode = {
particle: Input<Artboard>,
screenWidth: Input<number>,
screenHeight: Input<number>,
}
[...]After that, we'll initialize these values.
[...]
-- Return a factory function that Rive uses to build the Node instance.
return function(): Node<MyNode>
return {
init = init,
advance = advance,
update = update,
draw = draw,
particle = late(),
screenWidth = 850,
screenHeight = 1650,
}
endOnce the script is saved, we'll see two new input fields appear. Now we can move on to the fun part, working with the AI Agent!
We'll use the agent to animate the particle, create duplicates, and trigger various actions, allowing us to build a fully dynamic particle system using nothing but scripting.
Building the Falling Animation with the AI Agent
To begin, we'll use a simple prompt just to confirm everything is set up correctly. In this case, we'll tell the script to position the particles at the exact center of the screen.
To interact with the agent, open the agent tab and enter a prompt. Here's the one I used:
Modify the existing 'CoffeeParticlesFactory' script.
Center the Particle artboard in the middle of the screen.
Use the existing variables:
- screenWidth
- screenHeight
Set the Particle position so it is centered on both X and Y based on these values.With that, we've created our first bit of logic using the AI agent.
Below is the script generated by the AI agent:
[...]
-- Called every frame (after advance) to render the content.
function draw(self: MyNode, renderer: Renderer)
-- Center the particle artboard on the screen
renderer:save()
renderer:transform(
Mat2D.withTranslation(self.screenWidth / 2, self.screenHeight / 2)
)
self.particle:draw(renderer)
renderer:restore()
end
[...]Now, let's build a single animation for the particle. We'll tell the agent to generate a looping animation where the particle begins just above the top edge of the screen.
From there, it will fall all the way to the bottom over a duration of 3 seconds.
Once it reaches the bottom, the animation finishes and instantly starts over, looping endlessly.
Here is the second prompt:
Add a looping falling animation to the Particle artboard.
Requirements:
- The animation should loop continuously.
- Duration: 3 seconds.
- Start the particle above the top edge of the screen (outside bounds).
- End the particle below the bottom edge of the screen (outside bounds).
- Use ease in for the motion.
- When the animation loops, the particle should jump back to the starting position above the screen, without visible popping.Here is the animation when played:
Now that we have a single animation in place, let's duplicate it and transform it into a full particle effect.
Building a Particle Effect
To make this more dynamic, we'll introduce inputs that let us control the number of particles displayed on the screen and randomize their initial positions so they're evenly distributed rather than clustered together. We'll also stagger the animation so the particles begin falling at different moments.
First, create a new input named particlesCount and set its initial value to 10.
-- Define the script's data and inputs.
type MyNode = {
particle: Input<Artboard>,
screenWidth: Input<number>,
screenHeight: Input<number>,
[...]
particlesCount: Input<number>,
}
[...]
-- Return a factory function that Rive uses to build the Node instance.
return function(): Node<MyNode>
return {
init = init,
advance = advance,
update = update,
draw = draw,
particle = late(),
screenWidth = 500,
screenHeight = 500,
[...]
particlesCount = 10,
}
endNext, we'll move to the agent and tell it to duplicate the particle artboard according to the particlesCount input we just added.
We'll also have it assign a random X and Y position to each particle, ensuring they're scattered across the screen instead of dropping along a single path.
Here is the prompt I used:
Modify the existing 'CoffeeParticlesFactory' script to spawn multiple particles based on 'particlesCount'.
Requirements:
- Create 'particlesCount' particles (instances) of the Particle artboard.
- Each particle should use the existing falling animation with a loop duration of 3 seconds.
- For each particle, randomize a different X position across the screen width.
- Randomize a different Y offset so particles start at different times (staggered, not synchronized).
Loop behavior:
- When a particle exits the bottom of the screen (end of the 3s loop), reset it to the top (outside bounds) and re-randomize its X position so the falling pattern keeps changing over time.
Keep everything else unchanged.At this point, the single particle we designed is replicated into multiple particles. The end result is a simple falling animation made up of 10 individual particles.
If you're curious, I've made this animation available on the Rive Marketplace, where you can check it out here.
Adding the Animation to the App
Before the app can use the animation, it needs to be exported.
To do this, select Export > For runtime.
This generates a .riv file, which I'll name particles.riv.
Next, I created a view and initialize a RiveViewModel using the exported Rive file.
This is what the view implementation looks like:
import RiveRuntime
import SwiftUI
struct ScriptingView: View {
@State private var vm: RiveViewModel?
var body: some View {
ZStack {
Group {
if let vm = vm {
vm.view()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
} else {
VStack {
Text("Missing .riv file")
.font(.headline)
Text("Add `particles.riv` to the app target (Copy Bundle Resources), then rebuild.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
}
}
.navigationTitle("Scripting")
.onAppear {
// avoid RiveRuntime fatalError in SwiftUI previews when the resource isn't bundled yet.
if Bundle.main.url(forResource: "particles", withExtension: "riv") != nil {
vm = RiveViewModel(fileName: "particles")
}
}
}
}Rive Scripting is supported in the Rive iOS runtime starting from version 6.13.0, so be sure you're on that version or newer. Anything older won't work. Speaking from experience ... I learned that the hard way ๐
And here's the final outcome:
In case you are interested in the source code, feel free to check out the repository.
๐ค Wrapping Up
Rive Scripting was recently made available to everyone, and I wanted to dive into it and share a beginner-friendly overview with you. It's definitely a powerful on-ramp for designers and a speed boost for developers!
You can build fully interactive animations without needing any programming background. That brings the "Start building with Rive for iOS" series to a close!
I hope you enjoyed it as much as I did ๐
References

Thank you for reading this issue!
I truly appreciate your support. If you have been enjoying the content and want to stay in touch, feel free to connect with me on your favorite social platform:






