관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 콘페티, 폭죽, 눈 효과 (confetti, konfetti, CAEmitterLayer, CAEmitterCell) 본문

iOS 응용 (swift)

[iOS - swift] 콘페티, 폭죽, 눈 효과 (confetti, konfetti, CAEmitterLayer, CAEmitterCell)

jake-kim 2024. 5. 20. 01:53

콘페티 효과

  • 콘페티 효과를 구현하려면 CAEmitterCell과 CAEmitterLayer를 사용하여 구현
  • 폭죽 효과

  • 눈 효과

 

CAEmitterCell, CAEmitterLayer 개념

  • CAEmitterCell을 CAEmitterLayer에 추가하고, layer에서 위치를 정한다음 view.layer.addSublayer에 추가하여 구현
    • CAEmitterCell로 파티클에 들어갈 하나하나의 요소의 속성값을 정하고, CAEmitterLayer에 이 emitterCell을 주입해주는 것
let emitterCell = (CAEmitterCell 인스턴스)
let emitterLayer = CAEmitterLayer()
emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: UIScreen.main.bounds.height)
emitterLayer.emitterCells = [emitterCell]
view.layer.addSublayer(emitterLayer)
  • CAEmitterCell 개념
    • cell에는 image, birthRate, velocity, scale등과 같은 속성이 존재

https://developer.apple.com/documentation/quartzcore/caemittercell

  • CAEmitterLayer 개념
    • CALayer를 상속받아서 있는 컴포넌트

https://developer.apple.com/documentation/quartzcore/caemitterlayer

콘페티 구현 방법

  • CAEmitterCell에 콘페티 하나하나의 요소들에 대해 설정값을 정의
    • extension으로 정의하고, config를 따로 만들어서 사용하는 곳에서 원하는 값을 넣어서 사용할 수 있도록 구현
// Config는 아래에서 정의

extension CAEmitterCell {
    static func item(with config: Config) -> CAEmitterCell {
        let cell = CAEmitterCell()
        cell.name = config.id
        cell.contents = config.image.cgImage
        cell.color = config.color?.cgColor
        cell.beginTime = CACurrentMediaTime()
        cell.duration = config.duration
        cell.birthRate = config.birthRate
        cell.lifetime = config.lifeTime
        cell.velocity = config.velocity
        cell.velocityRange = config.velocityRange
        cell.emissionRange = .pi * config.emissionRange
        cell.emissionLongitude = config.emissionLongitude
        cell.scale = config.scale
        cell.scaleRange = config.scaleRange
        cell.scaleSpeed = 0
        cell.spin = config.spinVelocity
        cell.spinRange = config.spinRange
        cell.yAcceleration = config.accelerationOfGravity
        cell.setValue("plane", forKey: "particleType")
        cell.setValue(config.spinOrientationRange, forKey: "orientationRange")
        cell.setValue(config.spinOrientationLongitude, forKey: "orientationLongitude")
        cell.setValue(config.spinOrientationLatitude, forKey: "orientationLatitude")
        return cell
    }
}
  • Config는 아래처럼 preset도 가지고 있는 형태로 구현
struct Config {
    enum PresetType {
        case firework
        case snow
        
        var value: Config {
            switch self {
            case .firework:
                    .init(
                        id: "firework",
                        image: UIImage(named: "firework")!,
                        color: UIColor(
                            white: 1,
                            alpha: 0.8
                        ),
                        duration: 2.5,
                        birthRate: 50,
                        lifeTime: 10.5,
                        velocity: 500,
                        velocityRange: 50,
                        emissionRange: 0.25,
                        emissionLongitude: 0,
                        scale: 0.01,
                        scaleRange: 0.2,
                        spinVelocity: 1,
                        spinRange: 0.5,
                        accelerationOfGravity: 200,
                        spinOrientationRange: .pi,
                        spinOrientationLongitude: .pi,
                        spinOrientationLatitude: 0
                    )
            case .snow:
                    .init(
                        id: "snowfall",
                        image: UIImage(named: "firework")!,
                        color: UIColor(
                            white: 1,
                            alpha: 0.8
                        ),
                        duration: 10,
                        birthRate: 10,
                        lifeTime: 60,
                        velocity: 50,
                        velocityRange: 20,
                        emissionRange: .pi,
                        emissionLongitude: 0,
                        scale: 0.1,
                        scaleRange: 0.05,
                        spinVelocity: 0,
                        spinRange: 0,
                        accelerationOfGravity: 200,
                        spinOrientationRange: 0,
                        spinOrientationLongitude: 0,
                        spinOrientationLatitude: 0
                    )
            }
        }
    }
    
    /// 셀의 고유 식별자
    let id: String
    /// 셀에 표시되는 이미지
    let image: UIImage
    /// 셀의 색상
    let color: UIColor?
    /// 셀의 지속 시간: emitter의 활성화된 시간을 결정
    let duration: TimeInterval
    /// 초당 생성량: 초당 생성되는 emitter의 수 비율을 결정
    let birthRate: Float
    /// 셀의 수명: emitter 각 하나가 살아있는 시간(초)
    let lifeTime: Float
    /// emitter의 속도: 이게 클수록 높이 튀어오름
    let velocity: CGFloat
    /// 속도 범위: emitter의 초기 속도에 대한 무작위성을 결정
    let velocityRange: CGFloat
    /// emitter 방출 각도 범위: emitter가 방출되는 각도의 범위를 결정
    let emissionRange: CGFloat
    /// emitter 방출 각도: emitter가 방출되는 방향을 결정
    let emissionLongitude: CGFloat
    /// 크기 비율: emitter의 크기를 결정
    let scale: CGFloat
    /// 크기 범위: emitter의 크기에 대한 무작위성을 결정
    let scaleRange: CGFloat
    /// 회전 속도: emitter의 회전 속도를 결정
    let spinVelocity: CGFloat
    /// 회전 범위: emitter의 회전 속도에 대한 무작위성을 결정
    let spinRange: CGFloat
    /// 중력 가속도: 중력 가속도를 설정 (양수 값은 아래로 향하는 중력을 표현)
    let accelerationOfGravity: CGFloat
    /// 회전 방향 범위: emitter의 회전 방향의 범위를 결정
    let spinOrientationRange: CGFloat
    /// 회전 방향 경도: emitter의 회전 방향의 경도를 결정
    let spinOrientationLongitude: CGFloat
    /// 회전 방향 위도: emitter의 회전 방향의 위도를 결정
    let spinOrientationLatitude: CGFloat
}

 

사용하는 쪽)

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
//        showFirework()
        showSnowfall()
    }
    
    func showFirework() {
        let emitterCell = CAEmitterCell.item(with: Config.PresetType.firework.value)
        
        let emitterLayer = CAEmitterLayer()
        emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: UIScreen.main.bounds.height)
        emitterLayer.emitterShape = .line
        emitterLayer.emitterSize = CGSize(width: view.bounds.width, height: 1)
        emitterLayer.emitterCells = [emitterCell]
        
        view.layer.addSublayer(emitterLayer)
    }

    func showSnowfall() {
        let emitterCell = CAEmitterCell.item(with: Config.PresetType.snow.value)
        
        let emitterLayer = CAEmitterLayer()
        emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: 100)
        emitterLayer.emitterShape = .line
        emitterLayer.emitterSize = CGSize(width: view.bounds.width, height: 1)
        emitterLayer.emitterCells = [emitterCell]
        
        view.layer.addSublayer(emitterLayer)
    }
}

(완성)

콘페티 구현 시 주의할 점

  • 로컬에 있는 이미지를 사용할 경우, 디바이스의 스케일(UIScreen.main.scale)값에 따라서 크기가 다 다르게 나오므로 주의할 것
    • ex) UIImage의 인스턴스에서 size를 찍어봤을때 60*60의 로컬 이미지 scale을 0.1로 설정했을 때, x3 디바이스에서는 180*180으로 설정되므로 각 180 * 0.1 길이만큼 적용되고, x2인 디바이스에서는 120 * 0.1만큼 사이즈가 적용됨
    • 이 사이즈는 이미지 본연의 사이즈에 적용되므로 디바이스마다 크기가 다르게나옴
    • 때문에 디바이스별로 scale을 다르게 가져갈 것

 

* 전체 코드: https://github.com/JK0369/ExConfetti

* 참고

- https://stackoverflow.com/questions/43672353/swift-3-rotate-map-with-user-direction

- https://nsios.tistory.com/53

- https://developer.apple.com/documentation/quartzcore/caemitterlayer

- https://developer.apple.com/documentation/quartzcore/caemittercell

Comments