First of all, I have a page whose background color changes when I press the screen.
And I want to create a bubble with "+1" written on it, based on the background color. You can think of this as Instagram's reels video liking animation. When you double click, a heart appears where you clicked. What I want is for a +1 balloon/text to appear where I press for two seconds. But I'm stuck. I wanted to get some AI help, but the situation got worse.
Here is my current code if it helps:
import SwiftUI
struct Balloon: Identifiable {
let id = UUID()
var position: CGPoint
var text: String
var textColor: Color
}
struct TryPage: View {
@State private var displayName = "MeNaCa"
@State private var backgroundColor = Color.black
@State private var tapCount = UserDefaults.standard.integer(forKey: "tapCount")
@State private var balloons: [Balloon] = []
var body: some View {
NavigationStack {
ZStack {
backgroundColor
.edgesIgnoringSafeArea(.all)
.onTapGesture { self.handleTap(location: nil) }
.gesture(
DragGesture(minimumDistance: 0)
.onEnded { value in
self.handleTap(location: value.location)
}
)
Text(displayName)
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
ForEach(balloons) { balloon in
Text(balloon.text)
.foregroundColor(balloon.textColor)
.position(balloon.position)
.transition(.opacity)
}
}
}.background(ContentViewBindingView(bindingDisplayName: $displayName))
}
func handleTap(location: CGPoint?) {
guard let location = location else { return }
balloons.append(Balloon(position: location, text: "+1", textColor: self.textColorForBackground(self.backgroundColor)))
changeBackgroundColor()
incrementTapCount()
triggerHapticFeedback()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if let index = self.balloons.firstIndex(where: { $0.position == location }) {
self.balloons.remove(at: index)
}
}
}
func changeBackgroundColor() {
backgroundColor = Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
func incrementTapCount() {
tapCount += 1
UserDefaults.standard.set(tapCount, forKey: "tapCount")
}
func triggerHapticFeedback() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
}
func textColorForBackground(_ background: Color) -> Color {
let brightness = background.luminance
return brightness > 0.5 ? .black : .white
}
}
extension Color {
var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
let uiColor = UIColor(self)
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
return (red, green, blue, alpha)
}
var luminance: Double {
let (red, green, blue, _) = self.components
return (0.2126 * Double(red) + 0.7152 * Double(green) + 0.0722 * Double(blue))
}
}
The problem with this code is that the screen color does not change quickly when I press it. It used to change.
How do we solve it?
I have a custom ViewModifier
and an extension
to create Particle Effect like the one you are looking for. First create the Particle
strcut:
struct Particle: Identifiable {
var id: UUID = .init()
var randomX: CGFloat = 0
var randomY: CGFloat = 0
var scale: CGFloat = 1
var opacity: CGFloat = 1
mutating func reset() {
randomX = 0
randomY = 0
scale = 1
opacity = 1
}
}
Then create the ViewModifier
:
fileprivate struct ParticleModifier: ViewModifier {
var circleSize: CGFloat
var text: String
var font: Font
var status: Bool
var activeTint: Color
var inactiveTint: Color
var textColor: Color
/// View properties
@State private var particles = [Particle]()
func body(content: Content) -> some View {
content
.overlay(alignment: .top) {
ZStack {
ForEach(particles) { particle in
Circle()
.frame(width: circleSize, height: circleSize)
.overlay(alignment: .center) {
Text(text)
.font(font)
.foregroundStyle(textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
.foregroundStyle(status ? activeTint : inactiveTint)
.scaleEffect(particle.scale)
.offset(x: particle.randomX, y: particle.randomY)
.opacity(particle.opacity)
/// Only visible when status is active
.opacity(status ? 1 : 0)
/// Base Visibility with zero animation
.animation(.none, value: status)
} //: LOOP
} //: ZSTACK
.onAppear {
// Adding base particles for animation
if particles.isEmpty {
// Change to fit your needs
// particles = Array(repeating: Particle(), count: 15)
for _ in 0...15 {
particles.append(Particle())
}
}
}
.onChange(of: status) { oldValue, newValue in
if !newValue {
// Reset animation
resetParticles()
} else {
// Run animation
runAnimation()
}
}
}
}
//MARK: - FUNCTIONS
func runAnimation() {
for index in particles.indices {
/// Random Z & Y calculation based on index
let total: CGFloat = CGFloat(particles.count)
let progress: CGFloat = CGFloat(index) / total
let maxX: CGFloat = (progress > 0.5) ? 100 : -100
let maxY: CGFloat = 60
let randomX: CGFloat = ((progress > 0.5 ? progress - 0.5 : progress) * maxX)
let randomY: CGFloat = ((progress > 0.5 ? progress - 0.5 : progress) * maxY) + 35
let randomScale: CGFloat = .random(in: 0.35...1)
withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7)) {
/// Extra random values for spreading particles
let extraRandomX: CGFloat = (progress < 0.5 ? .random(in: 0...10) : .random(in: -10...0))
let extraRandomY: CGFloat = .random(in: 0...30)
particles[index].randomX = randomX + extraRandomX
particles[index].randomY = -randomY - extraRandomY
}
withAnimation(.easeIn(duration: 0.3)) {
particles[index].scale = randomScale
}
// Removing particles
withAnimation(.interactiveSpring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7).delay(0.25 + Double(index) * 0.0005)) {
particles[index].scale = 0.001
}
}
}
func resetParticles() {
for index in particles.indices {
particles[index].reset()
}
}
}
Finally the View extension
to use it with more ease:
extension View {
func particleEffect(circleSize: CGFloat = 20, text: String, font: Font, status: Bool, activeTint: Color, inactiveTint: Color, textColor: Color) -> some View {
self
.modifier(ParticleModifier(circleSize: circleSize,
text: text,
font: font,
status: status,
activeTint: activeTint,
inactiveTint: inactiveTint,
textColor: textColor)
)
}
}
As you can see it is pretty customisable too. You can pass the circle color, font of the text, text color etc.
You then need to edit your code to use the new extension
:
/// Add these two properties to your View
@State private var showBalloons = false
@State private var balloonOrigin: CGPoint = .zero
ZStack {
backgroundColor
.edgesIgnoringSafeArea(.all)
.onTapGesture { self.handleTap(location: nil) }
.gesture(
DragGesture(minimumDistance: 0)
.onEnded { value in
self.handleTap(location: value.location)
}
)
Text(displayName)
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(.white)
ForEach(balloons) { balloon in
Text(balloon.text)
.foregroundColor(balloon.textColor)
.transition(.opacity)
.position(balloon.position)
}
/// Add this here. It won't work in the for loop.
Color.clear
.frame(width: 100, height: 10)
.particleEffect(text: "+1", font: .caption2, status: showBalloons, activeTint: .white, inactiveTint: .accentColor, textColor: .black)
.position(balloonOrigin)
}
I needed to create two State
variables to trigger the bubble effect and to make the bubbles appear at the right spot.
And edit your handleTap
function like this:
func handleTap(location: CGPoint?) {
guard let location = location else { return }
balloons.append(Balloon(position: location, text: "+1", textColor: self.textColorForBackground(self.backgroundColor)))
changeBackgroundColor()
incrementTapCount()
triggerHapticFeedback()
showBalloons = true
balloonOrigin = location
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if let index = self.balloons.firstIndex(where: { $0.position == location }) {
self.balloons.remove(at: index)
}
showBalloons = false
}
}
Here's the reult:
Let me know your thoughts!