【SwiftUI】UIBezierPathでButtonの周りにオーディオスペクトラムを描画する

# はじめに
マイクアイコンをタップして音声を入力するとき、入力中であることをユーザーに知らせる必要があると思います。その際に、マイクボタンの周りにオーディオスペクトラムのようなものが描画されているとカッコ良いのではということで作ってみました（下図参照）。
マイクボタンの周りに放射状に広がるオーディオスペクトラムが描画されています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655077/0964e1c7-0625-3524-4408-9ee2888f053b.gif" width="300" align="center">

# 開発環境
- macOS Monterey 12.5.1
- Xcode 13.4.1
- Swift 5
- iOS 16 and later

# GitHub
https://github.com/mlballack/CircleAudioSpectrumButton

# 実装部分
実際に利用する場合は、`CircleSpectrum`を`Button`の下に配置します。
`value`の値の変化に応じて、スペクトラムの描画がアップデートされる仕組みです。
```swift:ContentView.swift
struct ContentView: View {
    @State var value: CGFloat = 0
    private let kButtonWidth: CGFloat = 60
    
    var body: some View {
        ZStack {
            Button {
                print("tap")
                Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { _ in
                    self.value = CGFloat.random(in: 0.5...1)
                }
            } label: {
                Image(systemName: "mic")
                    .foregroundColor(.black)
                    .font(.system(size: 24))
                    .frame(width: kButtonWidth, height: kButtonWidth, alignment: .center)
            }
            .background(Color(UIColor.systemGroupedBackground))
            .cornerRadius(kButtonWidth*0.5)
            .shadow(color: .gray, radius: 3, x: 3, y: 3)

            // Buttonの下に配置
            CircleSpectrum(
                value: value,
                maxAmplitude: 60,
                widthOfButton: kButtonWidth)
            .stroke(lineWidth: 0.5)
        }
    }
}
```
`CircleSpectrum`の実装部分です。実装イメージとしては、下図の黒線部分をUIBezierPathで描画しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655077/98856d40-6cbf-7691-3585-405a39c68076.png)

```swift
struct CircleSpectrum: Shape {
    @State var value: CGFloat
    
    public var maxAmplitude: CGFloat
    public var widthOfButton: CGFloat
    
    private let range = 360
    private let bin = 20
    private let stepTheta: CGFloat = 2.0

    func createSpectrumPath(rect: CGRect) -> Path {
        let path = UIBezierPath()
        let amplitude:CGFloat = maxAmplitude * value
        let thetaBlock = CGFloat(range / bin)

        let x0 = rect.midX
        let y0 = rect.midY

        var degree:CGFloat = 0.0
        
        for index in 1...bin {
            let degree0 = degree
            degree = CGFloat(index) * thetaBlock
            let degree1 = degree - stepTheta
            let theta0 = degree0 * CGFloat.pi / 180
            let theta1 = degree1 * CGFloat.pi / 180

            let random = CGFloat.random(in: 0.1...1.0)
            let L:CGFloat = amplitude * random
            let radius = widthOfButton * 0.5
            
            let p_0 = CGPoint(x: x0 + radius * cos(theta0),
                              y: y0 - radius * sin(theta0))
            let p_1 = CGPoint(x: x0 + (radius + L) * cos(theta0),
                              y: y0 - (radius + L) * sin(theta0))
            let p_2 = CGPoint(x: x0 + (radius + L) * cos(theta1),
                              y: y0 - (radius + L) * sin(theta1))
            let p_3 = CGPoint(x: x0 + radius * cos(theta1),
                              y: y0 - radius * sin(theta1))
            path.move(to: p_0)
            path.addLine(to: p_1)
            path.addLine(to: p_2)
            path.addLine(to: p_3)
        }
        return Path(path.cgPath)
    }
    
    func path(in rect: CGRect) -> Path {
        return createSpectrumPath(rect: rect)
    }
}
```

# 参考
- [How to use UIBezierPath and CGPath in SwiftUI](https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-uibezierpath-and-cgpath-in-swiftui)
- [【SwiftUI vs. Jetpack Compose】吹き出しテキストの作成(iOS版、Android版)](https://qiita.com/yuppejp/items/92429a0fc8440f9da487)