# ? Rendering Waveforms in SwiftUI

Episode #447 | 28 minutes | published on July 9, 2020 | Uses Swift-5.3, iOS-14.0-beta2, Xcode-12-beta2
Subscribers Only
I've been working on rendering waveforms using mathematical functions and have found the experience to be both fun and enlightening. In this episode we will develop a method to render arbitrary functions using a Shape, then explore some mathematical concepts that can help us render a nice looking waveform that could be use to indicate activity in sound, speech, or other effects.

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)
}
}
``````

## 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 {
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))
}
}.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)
}
}
}
``````