This video is only available to subscribers. Start a subscription today to get access to this and 467 other videos.

# Rendering Waveforms in SwiftUI

How I built the SiriWaveJS library - The math included in this episode was inspired by this blog post, where Flavio De Stafano renders the original Siri waveform using JavaScript. I found this post to be incredibly helpful in understanding the concepts.

You can download the Grapher file I used if you want to follow along.

## Defining the WaveForm Shape

First we define a shape we can use which takes a function that will define the line we'll plot.

```
struct WaveFormShape: Shape {
let fn: (Double) -> Double = { _ in 0 }
let range: ClosedRange<Double> = -1...1
let steps: Int = 3
func path(in rect: CGRect) -> Path {
...
}
}
```

Next we define a computed property to render each point:

```
var points: [CGPoint] {
var points = [CGPoint]()
let xStride = (range.upperBound-range.lowerBound) / Double(steps-1)
for x in stride(from: range.lowerBound, through: range.upperBound, by: xStride) {
let y = fn(x)
let p = CGPoint(x: x, y: y)
points.append(p)
}
return points
}
```

This gives us an array of graph points. Next we need to apply this to our view space, and for that we'll need the rect to transform these points into:

```
private func normalizedPoints(in rect: CGRect) -> [CGPoint] {
let points = self.points
return points.enumerated().map { (offset, p) in
let screenX = CGFloat(offset) * rect.width/CGFloat(points.count - 1)
let screenY = rect.midY - (p.y * rect.height/2)
return CGPoint(x: screenX, y: screenY)
}
}
```

Here we define the `y`

range with 0 being in the middle of the graph, 1 at the top, and -1 at the bottom.

Finally we can implement our path function:

```
func path(in rect: CGRect) -> Path {
Path { p in
let points = normalizedPoints(in: rect)
p.addLines(points)
}
}
```

## Graphing a Tapered Sine Curve

We start with a SwiftUI view that takes a few parameters:

```
struct GraphView: View {
var amplitude: Double = 2
var frequency: Double = 4
var phase: Double = 0
var body: some View {
...
}
}
```

Then we define the function that we want to use. For this I'll define a configurable sine function:

```
private func sineFunc(_ x: Double) -> Double {
amplitude * sin(frequency * x - phase)
}
```

As well as a windowing or "tapering" function we can use to filter out values on the edges:

```
private func taperFunc(_ x: Double) -> Double {
let K: Double = 1
return pow(K/(K + pow(x, 4)), K)
}
```

Now we can define the body of our view, rendering the waveform shape with the above functions:

```
WaveForm(
fn: { sineFunc($0) * taperFunc($0) },
steps: 300,
range: (-2 * .pi)...(2 * .pi)
)
.stroke(Color.white, style:
StrokeStyle(lineWidth: 7, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [], dashPhase: 0)
)
```

## Displaying a tweakable interface

To show this graph with some configurable sliders, we'll create some bindings we can pass in and control with sliders.

```
struct ContentView: View {
@State var amplitude: Double = 1.0
@State var frequency: Double = 4.0
@State var phase: Double = 0
var body: some View {
ZStack {
LinearGradient.pinkToBlack
VStack {
GraphView(amplitude: amplitude, frequency: frequency, phase: phase)
.frame(height: 200)
.blendMode(.overlay)
VStack {
ParamSlider(label: "A", value: $amplitude, range: 0...2.0)
ParamSlider(label: "k", value: $frequency, range: 1...20)
ParamSlider(label: "t", value: $phase, range: 0...(.pi * 40))
}.padding()
}
}.edgesIgnoringSafeArea(.all)
}
}
```

Here we use a simple extension on `LinearGradient`

to give it a nice background:

```
extension LinearGradient {
static var pinkToBlack = LinearGradient(gradient: Gradient(colors: [Color.pink, Color.black]), startPoint: .top, endPoint: .bottom)
}
```

And we also use a simple `ParamSlider`

component to simplify the label, slider, and range for each of our parameters:

```
struct ParamSlider: View {
var label: String
var value: Binding<Double>
var range: ClosedRange<Double>
var body: some View {
HStack {
Text(label)
Slider(value: value, in: range)
}
}
}
```