Complete the Apple Calculator in SwiftUI Using MVVM

Part 2 — Business Logic

Ricardo Montemayor
Better Programming

--

In today’s tutorial, we are going to be building the Calculator’s business logic in Swift using the MVVM model with the best practices in mind.

Building the view is covered in part 1.

Moreover, it’s perfectly fine to start from here if you are not interested on building the views. Go ahead and download the starter project.

The Calculator API

Our objective is to make a Calculator model fully independent.

According to the Single Responsibility Principle, every module, class, or function should have a single responsibility.

A calculator performs calculations; so it should only be focused on receiving inputs, computing, and returning the result. It’s not responsible on anything view related.

This gives us the ability to use the Calculator in anything. We can use it in our already existing CalculatorView, in unit tests, in another completely different calculator view, in a CLI, etc.

To make this possible we need to declare an API. — what functions and properties the outside can access to make use of our Calculator?

Let’s think of what properties and functions are necessary from the outside to be able to fully operate a calculator (These will be our public properties and functions). — What is at our disposal using a real calculator?

For properties, we need a way to read the currently displayed number.

For functions, we need to perform an action for every ButtonType case.

Let’s start.

Inside Models, create a new Swift File named Calculator

Add the following boilerplate code:

The ViewModel

Before continuing with the Calculator, let’s first connect our view so we can start testing it right away.

Below CalculatorView.swift, create a new Swift File named CalculatorViewModel:

In CalculatorViewModel.swift, create a final class name ViewModel that conforms to the ObservableObject protocol and is inside our CalculatorView

Our ViewModel will include the buttonTypes order and an instance of the Calculator model. We’ll also need to add the necessary properties and functions to be able to operate the Calculator.

Here’s the code for CalculatorViewModel.swift:

In CalculatorView.swift, add a new @EnvironmentObject private property to reference our ViewModel.

Make sure to update the displayText and buttonPad components to get viewModel.displayText and viewModel.buttonTypes.

Here’s the code:

Now, we need to notify the ViewModel when a calculator button is pressed.

In CalculatorButton.swift, add the ViewModel as an Environment Object and add viewModel.performAction(for: buttonType) for the button’s action

Finally, in CalculatorApp.swift, create an instance of the ViewModel as an environmentObject

Environment Objects gives us access to the ViewModel in all of the subviews

Now our View and our ViewModel are fully connected! The View notifies the ViewModel a button was pressed, and the ViewModel updates the display text shown in the View.

The Calculator

We’ll now start adding the business logic to make the Calculator work. We will be adding each of our API functions one by one.

Set Digit

Go back to Calculator.swift

Before starting with the logic of setting digits, we are going to need these properties, so add them at the top:

At the bottom, add the following helper functions

When setting a digit, we need to:

  1. Check if you can add the digit (01 should not be possible).
  2. Convert newNumber Decimal to a String
  3. Append the digit to the end of the string, convert the String back to Decimal and assign its new value to newNumber

Here’s the code to set a digit:

We can press the digit buttons to set digits in the Calculator

Set Operation

We’ll need to create a new struct name ArithmeticExpression to facilitate the evaluation of arithmetic expressions.

Add this struct inside your Calculator model:

Add expression and result properties and update the number computed property

Now, for setting an operation we need to:

  1. Check if there is a number we can use (newNumber or previous result) and assign it to a new variable number
  2. Check if there is already an existingExpression, if there is, evaluate it using number and assign the result to number
  3. Assign new ArithmeticExpression with number and operation to expression
  4. Reset newNumber

Here’s the code:

Now our Calculator works for digits and operations. Let’s just add some more visual queues to know if an operation is currently active.

We’ll highlight the operation button if the user just pressed it.

Add the following helper function to Calculator.swift

Add the following helper function to CalculatorViewModel.swift

And finally, add the following helper functions to CalculatorButton.swift

In the same file, update the CalculatorButton button style’s foregroundColor and backgroundColor to get from the helper functions just added

We now have operation buttons highlighting!

We can now set operations that get highlighted when pressed

Evaluate

This one is simple.

To evaluate we need to:

  1. Unwrap newNumber and expression (expression contains the previous number and operation)
  2. Evaluate expression with newNumber and assign to result
  3. Reset expression and newNumber

Here’s the code:

We can now use the equals button to evaluate expressions

Set Percent

To set percent we need to:

  1. Check if newNumber or result is currently used
  2. Divide by 100 and assign the new value

Here’s the code:

We can now use the percent button

Toggle Sign

toggleSign() is very similar to setPercent(), as in we need to check if newNumber or result is currently used and apply an operation (add negative sign in this case).

However, there’s a tricky part. — We cannot add a negative sign to 0.

The solution?

We can add a negative sign to a String, specifically the displayText. We just need to know when and when not to insert the negative sign.

Like in elementary math, we are going to use carries (9 + 8 = 17, carry 1).

Add this property at the top, under private var result: Decimal?

private var carryingNegative: Bool = false

So, if newNumber or result exists, apply the negative sign, otherwise, make the carryingNegative property true. (Will remain true until newNumber is set)

Add this code to toggleSign()

The carryingNegative property is used in the helper function getNumberString()

Remember how when newNumber, expression.number, and result are nil, the default value is 0? Thanks to the carryingNegative property, we can insert the negative sign string to 0 to return “-0” displayText just as a visual queue that the negative sign is active.

Update getNumberString() with the following code:

Lastly, we need to deactivate the negative carry when newNumber is set.

Add this code to newNumber:

We can now toggle the negative sign and wait until a digit is set

Set Decimal

Setting a decimal might be the trickiest part of all.

We’ll need to add the carry logic to two things now. To the decimal point and the zeroes following the decimal point.

The Decimal Point

Our newNumber property is of type Decimal. So there is no such thing as a 5. or 15. , those are whole numbers. However, if the user presses the decimal button, we need to provide a visual queue that the decimal is being set. We need to update the displayText until we can set newNumber to its respective decimal number. We’ll use a new property named carryingDecimal for this.

Let’s run a quick example for inputting 5.2

  1. Set digit 5 (newNumber = 5 , carryingDecimal = false, and the displayText = “5”)
  2. Set decimal (newNumber = 5 , carryingDecimal = true, and the displayText = “5.”)
  3. Set digit 2 (newNumber = 5.2 , carryingDecimal = false, and the displayText = “5.2”)

Add carryingDecimal property

Add containsDecimal computed property

The actual setting decimal function is very simple:

  1. Check if number already contains a decimal, if it does, return
  2. Make the carryingDecimal property true

Here’s the code:

Finally, reset carryingDecimal when newNumber is set

We can now set non-zero digits with decimal

The Zeroes Following the Decimal Point

Now, we have a similar problem to solve. When setting zeroes after de decimal point, our newNumber property, being of Decimal type, will never add them. As for 5.40000 will always get converted back to 5.4 as it is not a String.

Similar to what we did with the decimal point, we need to create a new property named carryingZeroCount, to keep track of zeroes after the decimal point and append them when a non-zero digit is set. In the meantime, we will also show the zeroes in the displayText to provide a visual queue.

Let’s run it through an example to make it clearer. We want to input 2.003

  1. Set digit 2 (newNumber = 2, carryingDecimal = false, carryingZeroCount = 0, displayText = “2”)
  2. Set decimal(newNumber = 2, carryingDecimal = true, carryingZeroCount = 0, displayText = “2.”)
  3. Set digit 0 (newNumber = 2, carryingDecimal = true, carryingZeroCount = 1, displayText = “2.0”)
  4. Set digit 0 (newNumber = 2, carryingDecimal = true, carryingZeroCount = 2, displayText = “2.00”)
  5. Set digit 3 (newNumber = 2.003, carryingDecimal = false, carryingZeroCount = 0, displayText = “2.003”)

I know it’s getting somewhat confusing, but by doing this, we can avoid using string manipulation for inputting altogether, which brings its fair share of issues and complexities.

Add the carryingZeroCount property

Update the setDigit() function to:

Increments carrying zero count if contains decimal and digit is zero

And finally, add carrying zeroes to getNumberString()

We can now set zeroes with decimal

All Clear

Clearing all is simple. We just need to reset all of our currently used properties

Here’s the code:

We can now clear all

Clear

Notice when using the Apple Calculator, the AC (All Clear) button changes to C (Clear) button right after setting a digit or decimal? These buttons, might look like they do the same thing, resetting the display to 0, but in reality, they have different functionalities.

All clear reset everything including newNumber, expression, result, and carries.

Clear only reset the last entry, in our case, newNumber and its carries.

So, let’s say you want to calculate 5 + 3. You input 5 + 2 — Oops! Press clear button (5 + is still saved), input 3, result = 8. All good.

Let’s get to it, but first, add pressedClear property, we are going to need it.

Similarly to our allClear() function, add this code to clear()

We are not resetting expression or result

And update newNumber, so that when its set, we reset pressedClear

Let’s add a small improvement to number for a better user experience.

If the user just pressed clear or decimal, let’s show a 0, even if there is a result or expression active.

Update the number property with this code:

Show AC or C button in our view

Now, to know which of the two clear buttons to show in our view, we will need to create a computed property.

Add showAllClear computed property:

Go back to CalculatorViewModel.swift and update the buttonTypes computed property:

The clear and all-clear buttons now switch accordingly

Final Calculator

Here’s the code of our final Calculator model:

Wrap-Up

We are finally finished building the Apple Calculator. Thanks for pulling through.

We successfully encapsulated each component into its own functionality. The ViewModel is not responsible for making the calculations, it just informs the Calculator what the user is inputting and asks for the result.

The Calculator model we created can be used with any view now. We can even create unit tests without involving any other file.

Final result

You can find the source code for this part here.

Thanks for reading, I would really appreciate it if you can follow so I can keep creating more content like this :)

Resources:

  • You can find part 1, creating the view, here
  • You can find the Github repository here

--

--