the making of bill splitter app

Click to download project open source on my Github repo :)

This is one of those apps that are just useful all the time in everyday life! It’s a sister app of BMI calculator from the app challenge in Angela Yu’s course, the codes are similar but taken a step further.

To make it my own version, I modified the tip percentage to what I need the most based on the typical tips we do in my area. I also modified the texts in all the labels to my liking, I just love a simple and clean interface. One thought I debated was: Wouldn’t it be more flexible to make a sider that covers a larger range of possible tip percentage? I didn’t go that route in the end for two reasons:

One is that sliding to an exact percentage could be extra work aka less convenience than just clicking on one of the options, especially for people like myself who can be pretty clumsy but OCD about getting that exact number I want, so then it feels like a lot of work just to dial in the numbers, I can totally see myself and some folks slide it some random numbers like 21% or 17%, but on the tap machine for payment, their options usually end with an odd number except for 15% in my area. So having some typical fixed percentage is the way to go.

I might add more percentage options in the future if I were to put this app on the app store, then obviously I shall consider people in different areas that might have differently tip percentage options. But for the purpose of learning, I do find overly complicating things could get in the way and slow down the process of learning, definitely have done that before and learned from it, so nowadays I try to control my urge of trying to make things more polished so that I could get through the learning process a bit more efficiently.

The other reason is purely for the sake of learning how to trigger a percentage label to calculate the result, because the slider code was already used in the BMI calculator app, so instead of repeating the same code, it’s better to use a different method so that I could learn how to code something new.

I love how Angela Yu has the steps of making this app broken down so we could follow them. This is exactly the type of “handholding“ beginners like myself need. She even included screenshots along with the elements that we need to work on as well as the result screenshot of what it should look like by the end of this section.

One of the challenges of my learning journey is how to break down tasks into smaller chunks, I forwarded a note of what I wish to be covered in the upcoming Swift over Coffee podcast to one of my favorite hosts Mikaela Caron, looking forward to possibly learning some tips on that in the future episodes.

All that being said, in this blog, I will be more focusing on the breakdown step by step on the workflow of making the app.

First of all, we need two screens:

Calculate screen & Result screen

Regarding controller files, we should end up with two swift files, one is CalculatorViewController for the first screen, the other one is ResultsViewController for the second screen. There is no models, so this app has a very straightforward structure: storyboard plus two controller files.

For both screens, once all the labels and buttons are created in the storyboard, it’s time to create some IBOutlets for the labels and IBActions for the buttons. One thing to point out is that all the tip percentage labels should trigger some code responsible for when tip is changed. Stepper is just like any label or button which you can add and call its value as part of its properties just like how we call text or font as a property. The Calculate button (first screen) should eventually trigger the screen to segue to the Result screen. The Recalculate button (second screen) should trigger the app to go back to the first screen to start over.

Calculate screen

Result screen

Next couple of steps are all about figuring out how to write some code to:

  • Highlight only the tip percentage the user (sender) selects while NOT highlighting the others. This is done by making the tip percentage true or false. I initially did something silly and mapped out each button’s scenario with “if statement“, which would end up with more than 10 lines code…until realizing that I could simply let the sender do its thing by making all the buttons false…Well, we learn right?! This taught us one thing, think no further, SENDER RULES!!!

fifteenPctButton.isSelected = false
eighteenPctButton.isSelected = false
twentyPctButton.isSelected = false
sender.isSelected = true

One thing that stood out for me is, the text field doesn’t go away after entering the numbers…I didn’t discover this issue until I installed it on my physical phone because keyboard doesn’t pop up on the simulator…to fix this issue, I had to add this line of code before the percentage button codes mentioned above. This allows the keyboard to be dismissed triggered by user clicking on any of the percentage button.

billTextField.endEditing(true)
  • Convert percentage into a decimal. This is to prepare for future calculation since we can’t really calculate with the format of percentage. Something really important I learned here is “type correct“ or “type consistency“. If we want the tip to end up having a decimal format such as 0.10 when the button’s title number is divided by 100, this number has to be a Double so that the tip could end up as a Double. And in order to turn that number into a Double, we have to get a hold of that number without the percentage sign, aka dropping it with some code. I was amazed that such code exists as just “.dropLast ( )“.

let buttonTitle = sender.currentTitle!
let buttonTitleMinusPercentSign = String(buttonTitle.dropLast())
let buttonTitleAsNumber = Double(buttonTitleMinusPercentSign)!
tip = buttonTitleAsNumber / 100
  • Get a hold of the text of the split number (total party number) and convert that into 0 decimal (aka a whole number) String which is the type we need to display on the label. Here is something that took me a second to wrap my head around: isn’t the value from the SENDER when stepper is pressed the same value of the number of people? If so, why do we need the code for turning number of people into Integer? I understood it later when I realized that we need to use the number of people for future calculation of the final result of the bill, where we would need to have a Double / Double situation, and we need to turn number of people into a Double, so String wouldn’t work, the String needs to be converted into Integer which then can be turned into a Double, duh…

@IBAction func stepperValueChanged(_ sender: UIStepper) {
        
        splitNumberLabel.text = String(format: "%.0f", sender.value)
        numberOfPeople = Int(sender.value)
        
        }
  • Figure out how to calculate the total bill, turn it into a String to be displayed on the next screen and perform a segue for the second screen. Again, based on the same idea of “type correct“, it needs to be a Double / Double situation, we have to back-engineer this a little bit. But first of all, how do we calculate the result? For example, if our total bill is $10, split between 2 people, tip is 20% aka 0.2, we each pay (10 + 10 * 0.2) / 2, which is the same as written this way: 10 * (1 + 0.2) / 2, then we just need to translate that into code such as billTotal * (1 + tip) / numberOfPeople. Then turn numberOfPoeple into a Double, so we have

let result = billTotal * (1 + tip) / Double(numberOfPeople)

Let’s say we want the final result to be displayed as 2 decimal String. That means the equation has to be Double / Double, which is why we previously needed to turn the number of people into a Double although it’s an Integer, so now we just need to also turn the billTotal and tip into Double with some code like `billTotal = Double(bill)!` And tip is already a Double from the previous code:

let buttonTitleAsNumber = Double(buttonTitleMinusPercentSign)!
    tip = buttonTitleAsNumber / 100

Now turn the final result into a String with 2 decimal format:

finalResult = String(format: "%.2f", result)

Perform segue:

performSegue(withIdentifier: "goToResult", sender: self)
  • The final step of the first screen is to override the prepare func for the segue with our own code, here we need to call 3 new properties from destinationVC which we will be created in the second screen: result, tip, split. Their values are located on the first screen, so in order to get a hold of the values of these properties, we need to set them up on the first screen:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "goToResult" {
            let destinationVC = segue.destination as! ResultsViewController
            
            destinationVC.result = finalResult
            destinationVC.tip = Int(tip * 100)
            destinationVC.split = numberOfPeople
            
        }
    }
  • Next step is to go to the second screen, create 3 new variables as the new properties we just called in the first screen, give them a default value.

var result = "0.0"
var tip = 10
var split = 2
  • Replace the code in viewDidLoad() with our own code. Get a hold of the text from the label to display the amount each person needs to pay in the end. Get a hold of the text that summarize the whole situation by inserting into a String using code like this \ ( )

override func viewDidLoad() {
        super.viewDidLoad()
        
        totalLabel.text = result
        settingsLabel.text = "Split among part of \(split) with \(tip)% tip®"
        
    }
  • The last step is to trigger the screen to jump back to the first screen when Recalculate button is pressed:

@IBAction func recalculatePressed(_ sender: UIButton) {
        
        self.dismiss(animated: true, completion: nil)
        
    }

Structure reference

App Code:

// CalculatorViewController.swift

import UIKit

class CalculatorViewController: UIViewController {

    @IBOutlet weak var billTextField: UITextField!
    @IBOutlet weak var fifteenPctButton: UIButton!
    @IBOutlet weak var eighteenPctButton: UIButton!
    @IBOutlet weak var twentyPctButton: UIButton!
    @IBOutlet weak var splitNumberLabel: UILabel!

    var tip = 0.10
    var numberOfPeople = 2
    var billTotal = 0.0
    var finalResult = "0.0"

    @IBAction func tipChanged(_ sender: UIButton) {

        billTextField.endEditing(true)
        
        fifteenPctButton.isSelected = false
        eighteenPctButton.isSelected = false
        twentyPctButton.isSelected = false
        sender.isSelected = true

        let buttonTitle = sender.currentTitle!
        let buttonTitleMinusPercentSign = String(buttonTitle.dropLast())
        let buttonTitleAsNumber = Double(buttonTitleMinusPercentSign)!
        tip = buttonTitleAsNumber / 100

    }

    @IBAction func stepperValueChanged(_ sender: UIStepper) {

        splitNumberLabel.text = String(format: "%.0f", sender.value)
        numberOfPeople = Int(sender.value)

        }

    @IBAction func calculatePressed(_ sender: UIButton) {

        let bill = billTextField.text!

        if bill != ""{

            billTotal = Double(bill)!
            let result = billTotal * (1 + tip) / Double(numberOfPeople)
            finalResult = String(format: "%.2f", result)
            performSegue(withIdentifier: "goToResult", sender: self)

        }
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "goToResult" {
            let destinationVC = segue.destination as! ResultsViewController

            destinationVC.result = finalResult
            destinationVC.tip = Int(tip * 100)
            destinationVC.split = numberOfPeople

        }
    }
}

// ResultsViewController.swift

import UIKit

class ResultsViewController: UIViewController {

    @IBOutlet weak var totalLabel: UILabel!
    @IBOutlet weak var settingsLabel: UILabel!

    var result = "0.0"
    var tip = 10
    var split = 2

    override func viewDidLoad() {
        super.viewDidLoad()

        totalLabel.text = result
        settingsLabel.text = "Split among part of \(split) with \(tip)% tip®"

    }

    @IBAction func recalculatePressed(_ sender: UIButton) {

        self.dismiss(animated: true, completion: nil)

    }
}
Next
Next

the making of BMI Calculator app