Tip Calculator in SwiftUI

Episode #397 | 20 minutes | published on June 14, 2019 | Uses Xcode-11.0-beta1, Swift-5.1
Free Video
Now that we've seen a taste of SwiftUI, let's dive into a real example and build an app. We'll have a first look at @State variables we can use to creating a binding between our state and our UI, and we'll run into a few puzzling errors and see how we can coax Xcode into giving us the right error message.
Beta 2 Warning: This video was recorded with Beta 1. In Beta 2, there's an issue with TextFields that use formatters. To continue in Beta 2, you'll have to use the TextField without a formatter.
Beta 3 Update: This now works but required some minor code modifications. The source code link has been updated to work with Beta 3.

Tip Calculator in SwiftUI

Now that we've seen a taste of SwiftUI, let's dive into a real example and build an app. We'll have a first look at @State variables we can use to create a binding between our state and our UI, and we'll run into a few puzzling errors and see how we can coax Xcode into giving us the right error message.

Creating a Navigation View

To start with, we will make the Tip Calculator UI using basic view template. We will create a navigation view, containing a VStack with .navigationBarTitle and a few configuration settings.


struct ContentView : View {
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Spacer()

                segmentedTipPercentages

                Divider()

                summaryLine(label: "Tip:", amount: formattedTipAmount, color: .gray)
                summaryLine(label: "Total:", amount: formattedFinalTotal, color: .green)

                Spacer()
            }
            .background(Color(white: 0.85, opacity: 1.0))
            .edgesIgnoringSafeArea(.all)
            .navigationBarTitle(Text("Tip Calculator"))
        }
    }
}

Two-way Binding Feature

To enable two way binding between our property wrapper @State variable and UI, we will prefix the variable with property binding $. By prefixing the variable with $ we would allow the UI to get regenerated for any change in the variable value or allow to update the variable for the changes made in the view. To display the TextField in UI, we will set a few configurations along with number formatter set as .currency.


@State private var totalInput: Double? = 18.94

private var currencyFormatter: NumberFormatter = {
    let f = NumberFormatter()
    f.numberStyle = .currency
    return f
}()

TextField($totalInput, formatter: currencyFormatter)
    .font(.largeTitle)
    .padding()
    .background(Color.white)
    .multilineTextAlignment(.center)

Creating Segmented Control

To display the selected tip percentage, we will bind the @State variables to SegmentedControl. We will now display the tipPercentages using text control and to select a particular text we will set the tag property of control using an index as an identifier. Note that the code is extracted into smaller functions, this will enable Xcode to provide us with a correct error message at the right location.


@State private var totalInput: Double? = 18.94
private let tipPercentages = [0.15, 0.2, 0.25]

private var segmentedTipPercentages: some View {
    SegmentedControl(selection: $selectedTipPercentage) {
        ForEach(0..<tipPercentages.count) { index in
            Text(self.formatPercent(self.tipPercentages[index])).tag(index)
        }
    }
}

private func formatPercent(_ p: Double) -> String {
    percentageFormatter.string(from: NSNumber(value: p)) ?? "-"
}

private var percentageFormatter: NumberFormatter = {
    let f = NumberFormatter()
    f.numberStyle = .percent
    return f
}()

Displaying the Formatted Tip Amount

Finally, to display the tip amount and total amount, we will create an HStack view and set a few configurations for the text to be displayed.


private var tipAmount: Double {
    let total = totalInput ?? 0
    let tipPercent = tipPercentages[selectedTipPercentage]
    return total * tipPercent
}

private var formattedTipAmount: String {
    currencyFormatter.string(from: NSNumber(value: tipAmount)) ?? "--"
}

private var finalTotal: Double {
    (totalInput ?? 0) + tipAmount
}

private var formattedFinalTotal: String {
    currencyFormatter.string(from: NSNumber(value: finalTotal)) ?? "--"
}

private func summaryLine(label: String, amount: String, color: Color) -> some View {
    HStack {
        Spacer()
        Text(label)
            .font(.title)
            .foregroundColor(color)
        Text(amount)
            .font(.title)
            .foregroundColor(color)
        }.padding(.trailing)
}

blog comments powered by Disqus