Nitrox Calculator using SwiftUI (Part 3)

March 07, 20234 min read
Nitrox Calculator using SwiftUI (Part 3) - Featured image

So far in the Nitrox Calculator series we've created a basic SwiftUI project and defined the layout and the basic building blocks of our Nitrox Calculator:

In Part 3 we are going to do a little bit of math and wire up the calculations to the UI

Maximum Operating Depth (MOD)

The calculations for MOD are quite basic. We need:

  • The fraction of oxygen in the gas mix - fO2
  • The Partial Pressure of Oxygen (ppO₂) - ppO2
  • The measurement system - Metric or Imperial

For Metric we will use the following formula:

MOD(m)=10×[ppO2FO21]MOD(m) = 10 \times \left[ \frac{ppO_2}{FO_2} - 1 \right]

And for Imperial:

MOD(ft)=33×[ppO2FO21]MOD(ft) = 33 \times \left[ \frac{ppO_2}{FO_2} - 1 \right]

Note that the conversion in not very precise, 10m is supposed to be 32.8083ft. However, the typical formula that scuba divers learn uses rounded-up numbers, which also adds a little bit of conservatism. Our calculator will use the standard formulas so that we can get results closer to what the users expect when doing the calculations by hand.

Calculator class

Let's create a new Swift file, MODCalculator.swift, for the calculator class. As stated earlier, we'll need few input parameters:

class MODCalculator {
    func calculateMOD(fO2: Double, ppO2: Double, unit: Unit) -> Double {
        var mod = (ppO2 / (fO2 / 100.0) - 1) * 10.0

        if unit == .imperial {
            mod *= 3.3
        }

        return round(mod)
    }
}

At the end we round the result to the nearest integer.

Updating the UI

Time to integrate the calculator. There quite a few changes highlighted below - we'll cover them one by one:

  • 2: an instance of MODCalculator is initialized
  • 7: New @State variable to hold the calculated MOD
  • 20: Displaying the MOD
  • 28, 39, 46: Updating the MOD when fO2 or ppO2 is changed, and when the view appears
  • 50: The updateMOD function uses the calculator instance
struct MODCalculatorView: View {
    private let calculator = MODCalculator()

    @AppStorage("unit") var unit: Unit = Unit.metric
    @State var ppO2: Double = 1.4
    @State var fO2: Double = 21
    @State var mod: Double = 0

    var body: some View {
        VStack {
            Spacer()
            VStack {
                Text("Max Operating Depth")
                    .multilineTextAlignment(.center)
                    .font(.title)
                    .padding(4)
                Text("for").light()
                Text("EAN \(fO2, specifier: "%0.0f")").strong()
                Text("is").light()
                Text("\(mod, specifier: "%0.0f") \(unit == .metric ? "meters" : "feet")").strong()

                Slider(
                    value: $fO2,
                    in: 21...100,
                    step: 1)
                    .padding()
                    .onChange(of: fO2) { _ in
                    updateMOD()
                }

                Text("with ppO₂").font(.subheadline)

                Picker("ppO2", selection: $ppO2) {
                    Text("1.4").tag(1.4)
                    Text("1.5").tag(1.5)
                    Text("1.6").tag(1.6)
                }
                    .onChange(of: ppO2) { _ in
                    updateMOD()
                }
                    .pickerStyle(.segmented)
                    .padding()
            }
            Spacer()
        }.onAppear {
            updateMOD()
        }
    }

    func updateMOD() {
        self.mod = self.calculator.calculateMOD(fO2: fO2, ppO2: ppO2, unit: unit)
    }
}

Now it's all working together:

Result

Enhancements

The app is functionally complete, but we can do better...

When the user drags the slider to the left end we can display "Air" instead of EAN21, and when the user drags to 100 we can display "Oxygen". Let's replace Line 18 with the following block:

if fO2 == 21 {
    Text("Air")
        .strong()
} else if fO2 == 100 {
    Text("Oxygen")
        .strong()
} else {
    Text("EAN \(fO2, specifier: "%0.0f")")
        .strong()
}

and add some animations:

private let animationDuration = 0.4

// ...
Slider(
    value: $fO2.animation(.easeInOut(duration: animationDuration)),
// ...

func updateMOD() {
    withAnimation(.easeInOut(duration: animationDuration)) {
        self.mod = self.calculator.calculateMOD(fO2: fO2, ppO2: ppO2, unit: unit)
    }
}

Adding animations in SwiftUI is pretty easy. Just wrap a block of code in withanimation or add animation to a binding and it will magically animate the affected UI elements.

And finally, let's set the accent color. Go to Assets > AccentColor > Universal - and from the Inspector select systemGreenColor

Accent Color

Result

And this is the final result:

You can find the final code on GitHub


Profile picture

Written by Nik
Follow on Twitter


nikz.dev