Greetings, traveler!
CAEmitterLayer
is a valuable tool for creating particle effects like smoke, fire, snow, etc. Today, we will use it to make a spoiler view with particles in SwiftUI.
First, let’s create a UIView
subclass to use CAEmitterLayer
. We will override the layerClass
property with CAEmitterLayer
. Then, we must override the layer
property to use it in the layoutSubviews
function to specify the emitter position and size.
final class EmitterView: UIView {
override class var layerClass: AnyClass { CAEmitterLayer.self }
override var layer: CAEmitterLayer {
super.layer as! CAEmitterLayer
}
override func layoutSubviews() {
super.layoutSubviews()
layer.emitterPosition = .init(
x: bounds.size.width / 2,
y: bounds.size.height / 2
)
layer.emitterSize = bounds.size
}
}
Now, we can create a SwiftUI View
with the UIViewRepresentable
protocol.
import SwiftUI
struct SpoilerView: UIViewRepresentable {
var isEnabled: Bool
var numberOfParticles: Float = 8000
func makeUIView(context: Context) -> EmitterView {
let emitterView = EmitterView()
let emitterCell = CAEmitterCell()
emitterCell.contents = UIImage(named: "white-particle")?.cgImage
emitterCell.contentsScale = 2
emitterCell.emissionRange = .pi * 2
emitterCell.velocityRange = 20
emitterCell.lifetime = 1
emitterCell.alphaRange = 1
emitterCell.birthRate = numberOfParticles
emitterCell.scale = 0.02
emitterCell.alphaRange = 1
emitterView.layer.emitterShape = .rectangle
emitterView.layer.emitterCells = [emitterCell]
return emitterView
}
}
Here, we will create and configure our EmitterView
. We will use a white circle image as a particle and specify the number of particles.
We can use a bool variable to manage spoiler status. We will handle its visibility in the updateView
function.
import SwiftUI
struct SpoilerView: UIViewRepresentable {
var isEnabled: Bool
var numberOfParticles: Float = 8000
func makeUIView(context: Context) -> EmitterView {
...
}
func updateUIView(_ uiView: EmitterView, context: Context) {
defer {
uiView.layer.birthRate = isEnabled ? 1 : 0
}
guard isEnabled else { return }
uiView.layer.beginTime = CACurrentMediaTime()
}
}
ViewModifier
To apply this spoiler to any view, we can create a ViewModifier
.
struct SpoilerModifier: ViewModifier {
let isEnabled: Bool
let numberOfParticles: Float
func body(content: Content) -> some View {
content.overlay {
SpoilerView(
isEnabled: isEnabled,
numberOfParticles: numberOfParticles
)
}
}
}
And then, we can create an extension to activate it with just one line of code.
extension View {
func spoiler(
numberOfParticles: Float,
isEnabled: Binding<Bool>
) -> some View {
self
.overlay {
Rectangle()
.fill(.ultraThinMaterial)
.opacity(isEnabled.wrappedValue ? 1 : 0)
.blur(radius: 2)
.padding(-3)
}
.modifier(
SpoilerModifier(
isEnabled: isEnabled.wrappedValue,
numberOfParticles: numberOfParticles
)
)
.animation(.easeIn, value: isEnabled.wrappedValue)
.onTapGesture {
isEnabled.wrappedValue.toggle()
}
}
}
Here, we will use a Rectangle
with ultra-thin material to create a blurred overlay and add a tap gesture to change the spoiler status with animation.
Now, let’s use it.
struct ContentView: View {
@State private var isSpoilerEnabled = true
private let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
var body: some View {
Text(text)
.padding()
.spoiler(
numberOfParticles: Float(text.count * 20),
isEnabled: $isSpoilerEnabled
)
}
}
Nice!
Conclusion
This approach can be modified differently to create a flexible solution. The source code is available here.
If you enjoyed this article, please feel free to follow me on my social media: