Search code examples
swiftswiftui

I'm trying to create bubbles where I press on the screen


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?


Solution

  • 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:

    +1 Bubbles

    Let me know your thoughts!