iOS/iOS 지식

[iOS] UIBezierPath, CAShapeLayer로 꽃 그리기

홍루피 2025. 5. 7. 22:49

UIBezierPath와 CAShapeLayer를 알아보고 간단한 꽃을 한번 그려보도록 하겠음

지디가 생각나는 데이지 꽃을 그려보도록 하겠음

 

 

UIBezierPath

UIBezierPath는 한마디로 말하면 경로를 그리는 거임

이 경로라는 것은 직선, 곡선을 사용해서 직사각형, 타원 등등 그리고 싶은 걸 만들 수 있다는 걸 의미함

 

이걸 사용해서 저 꽃잎부터 그려보도록 하겠음. 가장 쉬운 방법은 타원을 여러개 붙이는 것임

그래서 타원(oval)으로 패스를 생성해보겠음. 타원을 그릴때 인자가 CGRect인데 저 사각형 안에 꽉차게 원을 그리겠다는 것임

넙적한 꽃잎모양이 되도록 90x30으로 만들어줌

UIColor.white.setFill()을 통해서 현재 패스의 배경을 설정해줌. 색칠 하기 전에 물감 뭍히는 느낌이라고 생각하면 됨

꽃잎은 하얀색으로 설정하도록 하겠음

let center = CGPoint(x: rect.midX, y: 150)
let radius = 30.0
let length = 90.0

let petalPath = UIBezierPath(
    ovalIn: CGRect(
        x: center.x,
        y: center.y - radius / 2,
        width: length,
        height: radius
    )
)
UIColor.white.setFill()

 

꽃잎은 360도로 감싸고 있으니까 총 각도를 2 * .pi로 정의하겠음 (.pi = 180도 의미)

3시방향부터 진행되기 때문에 -90도 (.pi / 2)를 해주면 12시방향부터 진행되도록 할 수 있음

 

for문으로 9번을 돌면서 각도를 바꿔가면서 그려줄 것임

근데 우리가 연달아서 계속 그리고 각도를 바꿔주고 있지 않음?
이 연다른 동작을 하기 위해 UIGraphicsGetCurrentContext로 현재 그래픽 컨텍스트를 가져와주도록 하겠음

 

그리기 순서는 아래와 같음

1. 그리기 전에 상태를 저장

2. 중심 기준으로 해당하는 각도만큼 돌리기

3. 이 돌아간 꽃잎을 다시 원래 자리에 갖다 놓기

4. UIBezierPath.fill()로 색 채우기

let totalAngle = 2 * CGFloat.pi
guard let context = UIGraphicsGetCurrentContext() else { return }

    for i in 0..<count {
        // 3시 방향부터 그려지기 때문에 12시부터 그려지도록 바꿈
        let angle = CGFloat(i) * (totalAngle / CGFloat(count)) - (.pi / 2)
        
        // 도형 뒤에 그릴 내용이 있으면,
        // 변환을 변경하기 전에 현재 상태를 저장
        context.saveGState()
        
        // 뷰의 원점을 조정
        context.translateBy(x: center.x, y: center.y)
        context.rotate(by: angle)
        context.translateBy(x: -center.x, y: -center.y)
        
        // 회전된 상태로 꽃잎을 그림
        petalPath.fill()
        
        // 다른 컨텐츠를 그리기 전 복원
        context.restoreGState()
    }

 

이런 형태가 되었음. 이제 가운데에 노란 원을 하나 그리면 오른쪽처럼 꽃을 완성할 수 있어짐

원그리는 함수는 뭘까하고 찾아보면 addArc라는 호를 그리는 함수가 나옴

이 호를 360도로 그려서 원을 만들어 줄 수 있음 

let path = UIBezierPath()
path.addArc(
    withCenter: center,
    radius: 30,
    startAngle: 0,
    endAngle: .pi * 2,
    clockwise: true
)

UIColor.systemYellow.setFill()

// 현재 그래픽 컨텍스에 색을 채움
path.fill()

 

* 번외로 공유하고 싶은 부분들은 아래에 표시함

더보기

+ 번외 상식

이 BezierPath를 그리는 부분은 주로 UIView의 drawRect에서 설정해줄 수 있음

이  drawRect는 뷰의 드로잉 사이클인 제약 조건의 설정, 레이아웃 설정 이후에 호출되며 직접 호출되어서는 안됨

드로잉 사이클에 대해 정리가 잘 되어있는 링크를 확인해보길 권장함

 

+ Squircle ImageView 만들기

UIBezierPath 곡선에 대한 이해를 키우고 싶으면 스쿼클 이미지뷰를 만들어보는 것을 추천함

링크를 참고해서 만들어보았는데 관심 있으신 분들은 만들어보시길 추천함 (저도 한땀한땀 만들어봄)

 

다 그리고 나니까 뭔가 심심해서 애니메이션을 주고 싶어짐

그래서 사용하게 된게 CAShapeLayer임. 이걸 사용해서 패스를 이 레이어에 설정한 다음 이 레이어에 애니메이션을 넣어줄 것임

CALayer를 상속받는 이 CAShapeLayer는 주로 도형을 표시할 때 사용함. GradientLayer같은 다른 종류의 레이어도 존재함

우리가 뷰에 기본적으로 레이어가 있고, 이 레이어 위에 서브레이어가 여러개가 올라갈 수도 있음

CAShapeLayer

 

CAShapeLayer와 CABasicAnimation를 사용하는 것이 메인임

아까는 연달아서 컨텍스트를 저장해가며 그렸다면,

지금은 각각의 꽃잎을 레이어로 만들고 이 레이어에 투명도를 조절하는 애니메이션을 달 것임

 

1. UIBezierPath로 타원을 만듬

2. 이 UIBezierPath를 CAShapeLayer 패스로 설정

3. CGAffineTransform(2차원 좌표변환)을 사용해서 각도만큼 꽃잎 돌리기

4. 레이어에 CABasicAnimation을 적용

   func drawPetalsWithLayer(_ rect: CGRect, _ count: Int) {
        let center = CGPoint(x: rect.midX, y: 500)
        let radius = 30.0
        let length = 90.0
        let totalAngle = 2 * CGFloat.pi
        
        for i in 0..<count {
            let angle = CGFloat(i) * (totalAngle / CGFloat(count)) - (.pi / 2)
            
            let petalLayer = CAShapeLayer()
            let petalPath = UIBezierPath(
                ovalIn: CGRect(
                    x: 0,
                    y: -radius / 2,
                    width: length,
                    height: radius
                )
            )
            petalLayer.path = petalPath.cgPath
            petalLayer.fillColor = UIColor.white.cgColor
            petalLayer.position = center
            petalLayer.setAffineTransform(CGAffineTransform(rotationAngle: angle))
            petalLayer.opacity = 0
            layer.addSublayer(petalLayer)
            
            // 애니메이션
            let appear = CABasicAnimation(keyPath: "opacity")
            appear.fromValue = 0
            appear.toValue = 1
            appear.duration = 0.3
            appear.beginTime = CACurrentMediaTime() + Double(i) * 0.3 // 순차적으로
            appear.fillMode = .forwards
            appear.isRemovedOnCompletion = false
            
            petalLayer.add(appear, forKey: "fadeIn")
        }
        
        // 가운데 노란 원 추가
        let centerLayer = CAShapeLayer()
        let centerPath = UIBezierPath(
            arcCenter: .zero,
            radius: 30,
            startAngle: 0,
            endAngle: .pi * 2,
            clockwise: true
        )
        centerLayer.path = centerPath.cgPath
        centerLayer.fillColor = UIColor.systemYellow.cgColor
        centerLayer.position = center
        centerLayer.opacity = 0
        
        layer.addSublayer(centerLayer)
        
        // 가운데 원 그리기
        let centerAppear = CABasicAnimation(keyPath: "opacity")
        centerAppear.fromValue = 0
        centerAppear.toValue = 1
        centerAppear.duration = 0.3
        centerAppear.beginTime = CACurrentMediaTime() + Double(count) * 0.3
        centerAppear.fillMode = .forwards
        centerAppear.isRemovedOnCompletion = false
        
        centerLayer.add(centerAppear, forKey: "fadeIn")
    }

 

결과물

 

 

참고자료