본문 바로가기

프로그래밍/iOS,macOS

[SwiftUI] 오디오 레벨 에니메이션, Shape

오디오 레벨이나 오브젝트 주변을 인지하기 쉽도록 하기위해 펄스 에니메이션이 사용되곤 한다.
보통 원형으로 스케일을 통해 에니메이션을 구현하는데~

제공 하는 Circle shape를 사용해 간단히 구현할 수 있다.
뷰의 state 나 뷰모델에서 level 값을 퍼블리싱 한다고 가정하면...

ZStack {
    Circle()
        .frame(width: 150, height: 150, alignment: .center)
        .foregroundColor(Color.yellow)
        .scaleEffect( (CGFloat(self.viewModel.level) / 100.0) + 1)
        .opacity(0.2)
        .animation(Animation.easeIn(duration: 0.1))

    Circle()
        .frame(width: 150, height: 150, alignment: .center)
        .foregroundColor(Color.yellow)
        .scaleEffect( (CGFloat(self.viewModel.level - 50) / 100.0) + 1)
        .opacity(0.8)
        .animation(Animation.easeIn(duration: 0.1))

    self.myCustomContentView
        .frame(width: 150, height: 150, alignment: .center)
        .clipShape(Circle())
}

view model의 level 값에 따라서 scaleEffect 가 발생하고, 특정 값에 따라 스케일 값을 정하면, 해당 생상의 shape 가 에니메이션 된다.

스케일 값을 어떻게 표시해주는지에 따라 다양한 모양의 연출이 가능하다.

단순한 shape 스케일이 아닌 다양한 라인을 그릴 수도 있을까?
위에 내부에 보이는 3개 라인의 레벨 미터도 한번 만들어 봤다.

레벨을 얻게 되면 특정값 아래는 거의 들리지 않으므로, 오디오 레벨에서 표시할 최소치와 최대치를 설정해 두고, 해당 범위 내의 값들만 표시.
메인 라인과 양옆 서브 라인으로 구성.

struct AudioLevelShape: Shape {
    let minLevel = 68
    let maxLevel = 85
    let subMaxLevel = 90

    let minHeight:CGFloat = 2.4
    let width:CGFloat = 2.4
    let mainMaxHeight:CGFloat = 15
    let subMaxHeight:CGFloat = 10



    var level: Int
    var animatableData: Int {
        get { level }
        set { level = newValue }
    }

    init(level: Int) {
        self.level = level
    }



    func path(in rect: CGRect) -> Path {
        var mainLevelRate:CGFloat = 0
        var subLevelRate:CGFloat = 0

        if level <= minLevel {
            mainLevelRate = 0
            subLevelRate = 0
        } else {
            if level > maxLevel {
                mainLevelRate = 1
            } else {
                mainLevelRate = min( 1, CGFloat(level - minLevel) / CGFloat(maxLevel - minLevel))
            }

            if level > subMaxLevel {
                subLevelRate = 1
            } else {
                subLevelRate = min( 1, CGFloat(level - minLevel) / CGFloat(subMaxLevel - minLevel))
            }
        }

        return Path { path in
            let mainHeight = mainMaxHeight * mainLevelRate + minHeight
            let subHeight = subMaxHeight * subLevelRate + minHeight

            let mainY = (rect.height / 2) - (mainHeight / 2)
            let subY = (rect.height / 2) - (subHeight / 2)

            let mainStart = CGPoint(x: rect.midX, y: mainY)
            let mainEnd = CGPoint(x: mainStart.x, y: mainY + mainHeight)

            let leftStart = CGPoint(x: rect.midX - ((width/2) + width), y: subY)
            let leftEnd = CGPoint(x: leftStart.x, y: subY + subHeight)

            let rightStart = CGPoint(x: rect.midX + ((width/2) + width), y: subY)
            let rightEnd = CGPoint(x: rightStart.x, y: subY + subHeight)

            path.move(to: mainStart)
            path.addLine(to: mainEnd)

            path.move(to: leftStart)
            path.addLine(to: leftEnd)

            path.move(to: rightStart)
            path.addLine(to: rightEnd)
        }
    }
}

 

실제 오디오 레벨 얻는 루틴이 없어서 타이머로 이벤트 발생되면 랜덤 값을 입력해 테스트.
프리뷰에서 보면 대략 아래와 같은 모습.

var timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
@State private var localAudioLevel: Int = 0

.
.
.
.



HStack {
    ZStack {
        self.myCustomContentView
            .clipShape(Circle())
            
        AudioLevelShape(level: self.localAudioLevel)
            .stroke(Color.white, style: StrokeStyle(lineWidth: 2.4, lineCap: .round))
            .frame(width: 30, height: 30, alignment: .center)
            .background(Color.orange)
            .clipShape(Circle())
            .onReceive(self.timer) { input in
                self.localAudioLevel = Int.random(in: 50..<120)
            }



    }
    .frame(width: 200, height: 200, alignment: .center)
}

막대형 레벨 미터는 특별히 에니메이션을 추가하지는 않았는데, 에니메이션을 제공하고 싶으면 shape 뷰에 
.withAnimation { self.localAudioLevel = 값 } 으로 설정해 주면, 에니메이션이 이루어진다.