In one of the previous posts, I shared a simple way of Creating particle effects in SwiftUI. The approach is super easy and utilizes the power of viewModifiers, but I would not recommend it for production use as it is performance-greedy when having a bigger amount of particles in place (because each particle is a single view)
In this post, I will introduce you to an alternate and better approach - rendering the particles with the Canvas view. So let’s get into it 💪.
Setup
We will start with the following view outline:
This view features an outer TimelineView that ensures its content (inner view) is regularly re-drawn. (note the .animation parameter allowing the system to decide the optimal refresh rate)
The content here is a Canvas view. Those of you who come from good old UIKit days might be already familiar with the concept of the drawing context. In simple words, we get a canvas area with the view dimensions and we can draw/rasterize
various entities on it - like shapes and images.
In our case, we will draw a single particle represented with a SingleParticleView. Please note, how the SingleParticleView is being used as a drawing element. It is added to a symbols parameter allowing SwiftUI to pre-render it and thus be very performant later in the drawing calls - thus an ideal candidate for the many particles in the place ;)
At this moment, let’s just set the SingleParticleView as an orange dot, but we will tune it soon
So far, we have managed to draw a small orange dot, but it is about to change soon ;)
I like to move it
Now, let’s move that particle.
I will build here a fire-ish effect blending multiple upwards moving particles - so as a good start let’s periodically move the single particle up from the canvas bottom:
You can see, that I am controlling the upwards movement with the time variable. What exactly is it in this context? Well, the timeline view already gives us access to the time property, but for our use, I want to have something normalized that can be easily bound with the particle movement.
I want the particle movement to take exactly 2 seconds (see movementDuration) so the code computes time as a truncating remainder, making sure it will periodically grow from 0 to 1 forever. As you can see in the following video:
Remember goniometry?
As a next step, we will upgrade the movement from the simple linear to something more firey :). My perception of fire movement is that it is waving, so let me change the code to move the particle along a cosinus wave whose amplitude is smaller the higher the particle is:
please note the position computation was moved to a separate function so the Canvas content remains clean.
Make it many
At this point, I am quite happy with the movement. I am sure we will fine-tune the constants here and there, but that can come later. Now, we want to draw more particles so let’s wrap the drawing into the for-cycle like this:
Randomize
We can finally see more particles, but they all share the same path, so let me initiate each particle with random starting wave rotation and starting time offset:
Improving the effect appearance
In terms of particle motion, I consider this done, but we still need to fine-tune this effect’s appearance to get some juiciness.
The first improvement is changing the particle opacity during the opacity movement - this is quite simple by changing the context opacity before a draw call:
Next, let’s utilize the blending capabilities of SwiftUI and set the particle appearance like this:
We make particles here as a nice big blurry spots, that blends together to form a fire volume. The blendMode(.plusLighter) combines overlapping orange dots, effectively brightening the result where the patrticles intersect.
At this point, I am still not very happy with the result and will need to introduce more tweaks. This is very typical in such a creative process that your initial idea is not exactly aligned with the implementation and you are required to iterate on it.
The thing that bothers me is that the particles are more dense at the top of the view, while I would prefer otherwise. To fix that, let me get rid of the even y-axis distribution and adjust the y-coordinate like this:
Also, I would like to boost the particle vanishing effect so let me decrease the particle opacity with time.
The final effect I am happy with is:
Your turn!
Now it is your time to get creative!
Things to try
change particle appearance
change particle movement paths
combine multiple particle types
react to user inputs
💫 …
Here is a result of more experiments and adjustments:
Enjoy! And let me know, if you find this article helpful, and send me your animations on Twitter.