Nitrox Calculator using SwiftUI (Part 2)

February 27, 20234 min read
Nitrox Calculator using SwiftUI (Part 2) - Featured image

In Part 1 we setup the project and the main application layout with a TabView control. Now we are moving to the Settings screen and the Maximum Operating Depth (MOD) Calculator screen

Views and Models

Let's start by creating views for the two tabs - MODCalculatorView and SettingsView. Select New File... and pick SwiftUI View. We'll also need a regular Swift File for our Model where we'll define a few types for the application settings.

New File

Create File

Now we can replace the placeholders in ContentView with the new views that we just created:

struct ContentView: View {
    var body: some View {
        TabView {
            MODCalculatorView().tabItem {
                Label("MOD", systemImage: "gauge")
            }

            SettingsView().tabItem {
                Label("Settings", systemImage: "gear")
            }
        }
    }
}

Model

We have a requirement that the app should support both Metric and Imperial measures. In Model.swift we will define a simple enum for Unit:

enum Unit: String, CaseIterable, Identifiable {
    case metric = "Metric"
    case imperial = "Imperial"

    var id: Self { self }
}

We explicitly have it defined as String and CaseIterable which will allow us to easily integrate it into a Picker

Settings View

Now let's define the SettingsView

struct SettingsView: View {
    @AppStorage("unit") var unit: Unit = Unit.metric

    var body: some View {
        NavigationStack {
            Form {
                Picker("Unit", selection: $unit) {
                    ForEach(Unit.allCases, id: \.self) { value in
                        Text(value.rawValue).tag(value)
                    }
                }
            }
                .navigationTitle("Settings")
        }
    }
}

@AppStorage is a handy property wrapper that can save and load a value from the UserDefaults. In this case, the default value is assigned to metric because 95% of the World uses the Metric System (sorry USA 😉), but hey, at least our little app will support (and remember your choice) if you decide to switch to Imperial.

It's all wrapped in a NavigationStack so that we can get a nice header for free, and the Picker is bound to the AppStorage unit variable.

Settings View

MODCalculator View

Now we can switch to the calculator view. From the wireframe in Part 1 we can see we need to center the UI, we need a few Text elements, a Slider, and a Segment Picker.

The Slider will update the fO2, fraction of Oxygen, and will have a default value of 21 (the value for Air) and a range between 21 and 100.

For ppO2, Partial Pressure of Oxygen (ppO₂), we'll have three possible values 1.4, 1.5, and 1.6.

Wireframe

Let's start with a very basic layout:

struct MODCalculatorView: View {
    @AppStorage("unit") var unit: Unit = Unit.metric
    @State var ppO2: Double = 1.4
    @State var fO2: Double = 21

    var body: some View {
        VStack {
            Spacer()
            VStack {
                Text("Max Operating Depth")
                Text("for")
                Text("EAN \(fO2, specifier: "%0.0f")")
                Text("is")
                Text("? \(unit == .metric ? "meters" : "feet")")

                Slider(
                    value: $fO2,
                    in: 21...100,
                    step: 1)
                    .padding()

                Text("with ppO₂")

                Picker("ppO2", selection: $ppO2) {
                    Text("1.4").tag(1.4)
                    Text("1.5").tag(1.5)
                    Text("1.6").tag(1.6)
                }
                    .pickerStyle(.segmented)
                    .padding()
            }
            Spacer()
        }
    }
}

VStack arranges the child elements vertically, and the two Spacer elements allow us to center the UI. The Slider value is bound to fO2 and the Picker is bound to ppO2. That's done using a special syntax with $ (dollar sign) on a Binding<T> property exposed by the control.

Preview

Not too bad for a start, but we can make it look a little bit nicer. Let's define two extensions on Text:

extension Text {
    func strong() -> some View {
        self.font(.title)
            .fontWeight(.bold)
            .padding(4)
    }

    func light() -> some View {
        self.font(.title2)
            .padding(4)
    }
}

and apply some styling on the text

Text("Max Operating Depth")
    .multilineTextAlignment(.center)
    .font(.title)
    .padding(4)
Text("for").light()
Text("EAN \(fO2, specifier: "%0.0f")").strong()
Text("is").light()
Text("? \(unit == .metric ? "meters" : "feet")").strong()
// ...
Text("with ppO₂").font(.subheadline)

Result

Result

Next

Next, we will implement the calculations and wire up the calculator screen...


Profile picture

Written by Nik
Follow on Twitter


nikz.dev