Gavin Wiggins

AboutNotesPhotos


Sine wave

Written on February 4, 2023

To draw a sine wave, create a shape that draws a line using x and y values. The values must be converted to the proper points within the view's frame. Markers represent points on the drawn line.

sine wave

import SwiftUI

struct MarkersView: View {

    let xValues: [Double]
    let yValues: [Double]

    var body: some View {
        GeometryReader { geom in
            let xMin = xValues.min() ?? 0.0
            let xMax = xValues.max() ?? 1.0

            let yMin = yValues.min() ?? 0.0
            let yMax = yValues.max() ?? 1.0

            ForEach(0..<yValues.count, id: \.self) { i in
                Circle()
                    .fill(.blue)
                    .frame(width: 8)
                    .position(
                        x: ((xValues[i] - xMin) * (geom.size.width)) / (xMax - xMin),
                        y: geom.size.height - ((yValues[i] - yMin) * (geom.size.height)) / (yMax - yMin)
                    )
            }
        }
    }
}

struct LineShape: Shape {

    let xValues: [Double]
    let yValues: [Double]

    func path(in rect: CGRect) -> Path {

        let xMin = xValues.min() ?? 0.0
        let xMax = xValues.max() ?? 1.0

        let yMin = yValues.min() ?? 0.0
        let yMax = yValues.max() ?? 1.0

        // Scale values to CGPoints based on formula at https://en.wikipedia.org/wiki/Normalization_(statistics)
        // X' = a + (X - Xmin)(b - a) / (Xmax - Xmin)
        let pts = zip(xValues, yValues).map { (x: Double, y: Double) -> CGPoint in
            let xn = ((x - xMin) * (rect.width)) / (xMax - xMin)
            let yn = rect.height - ((y - yMin) * (rect.height)) / (yMax - yMin)
            return CGPoint(x: xn, y: yn)
        }

        var path = Path()
        path.move(to: pts[0])

        for i in 1..<pts.count {
            path.addLine(to: pts[i])
        }

        return path
    }
}

struct SineView: View {

    @ObservedObject var data: Data

    var body: some View {
        ZStack {
            MarkersView(xValues: data.x, yValues: data.y)

            LineShape(xValues: data.x, yValues: data.y)
                .stroke(.red, lineWidth: 2.0)
                .border(.white.opacity(0.2))
        }
    }
}

The sine wave view is updated by adjusting the frequency and step values.

import SwiftUI

class Data: ObservableObject {

    @Published var freq: Double = 4
    @Published var step: Double = 0.05

    var x: [Double] {
        Array(stride(from: 0, to: 5, by: step))
    }

    var y: [Double] {
        x.map { sin(freq * $0) }
    }
}

struct ContentView: View {

    @StateObject private var data = Data()

    var body: some View {
        VStack {
            Slider(value: $data.freq, in: 2...10, step: 1.0)
            Text("freq is \(data.freq, specifier: "%.0f")")

            Slider(value: $data.step, in: 0.01...0.1, step: 0.01)
            Text("step is \(data.step, specifier: "%.2f")")

            SineView(data: data)
        }
        .padding()
        .frame(minWidth: 500, minHeight: 400)
    }
}

The Python example below provides a comparison to the Swift version. The Matplotlib Python package is used to create the plot.

sine wave

import numpy as np
import matplotlib.pyplot as plt

freq = 4
step = 0.1

x = np.arange(0, 5, step)
y = np.sin(freq * x)

_, ax = plt.subplots(tight_layout=True)
ax.plot(x, y, marker='.')
ax.set_xlabel('Angle [rad]')
ax.set_ylabel('sin(freq⋅x)')

plt.show()

Gavin Wiggins © 2024.
Made on a Mac with Genja. Hosted on GitHub Pages.