SECRET OF CSS

Build the Apple Calculator in SwiftUI | by Ricardo Montemayor | Jul, 2022


Creating the view, part 1

1*5cm w qzn1IH2v7O7R ZVw

In this tutorial series, we are going to build the Apple Calculator from scratch. This Calculator will be developed with the best practices in mind. If you are a beginner, that’s great, as you’ll start learning some new challenging concepts that will make you a better developer. If you are intermediate, this tutorial will help you grasp some important practices for building scalable and maintainable SwiftUI apps.

In this tutorial, we will start by building the view.

The view is one of the most important parts of your app as the view is what the user sees and interacts with. The view is also the part of your code most likely to change over time, so building a well-structured view is essential for maintainability.

We will recreate, as close as we can, the Apple Calculator, following some of the best practices to organize and maintain our views.

1*lDOP01a49JO0a7cP0DG8Xw
The finished product

Create a new iOS app.

1*sgDCOJ1jW7hWXKqoqMJuXA

Enter Calculator for Product Name and make sure you select SwiftUI as your Interface. Uncheck all the boxes at the bottom, we won’t need those functionalities for now.

Okay great! We have our project, let’s get coding. But wait! How can we start coding, if we do not know what we are going to build?

I cannot stress this enough. Before building anything, plan.

When building views make sure you have some sort of reference to work with for what you are going to build. This can be anything from a poorly drawn sketch to a pixel-perfect mockup. Anything that you can go back to and look at and give you an idea of what the result should look like is good.

1* ktnyl5bcjHyOS7ocs2RsA
Feel free to use this mockup I made as a reference

Fortunately, we are not designing our views from scratch, as we are building a recreation of an already existing app (for educational purposes). We can just open the calculator app on our iOS devices to look at the reference.

Okay, so now we need to start thinking about how to lay our views before writing any code.

Deconstructing a View

This step is essential for planning and knowing how we are going to implement our view in code.

The objective is to break down the view into just simple components.

When deconstructing a big complex view, always try to think of everything as a container. Start from the outside (general groupings) and slowly work your way inside until you get to the simple components.

1*IbPi
Containers within containers within a container

Remember, there are a thousand ways of implementing a view. This is my implementation, of what I thought was the simplest way to go about it.

So, going back to what I mentioned earlier about deconstructing the view — At the outermost layer, the CalculatorView, we can visualize a VStack which contains 3 components.

The Spacer and DisplayText are already deconstructed to their simplest form (A Spacer and a Text), however, the button pad has a lot of extra complexity we need to deconstruct. — Let’s see what we can come up with.

Focus only on the ButtonPad as if it is the only view that exists. This is a very important engineering principle; breaking problems into more digestible subproblems. We are going to be applying this principle throughout the series.

We can see now, another VStack (this one has smaller spacing than its parent) which contains multiple stack views. Each HStack contains 3 or 4 Button views. Button is the simplest form we can go so we are done!

We have fully deconstructed Apple’s Calculator view.

1*i76pThFzzZSAXJ8c5991EA

Perfect, we have successfully deconstructed the view, and we now have an idea of what the implementation will look like.

We will now start constructing the view.

Constructing a View

Constructing a view is no longer part of the planning phase. — We are now building the actual app.

To construct a complex view, we will apply the same principle of breaking the problem into smaller, more digestible subproblems. You can start from the innermost layer and work your way out, or you can start from the outermost layer and work your way in.

I like using a combination of both as it gives me the big picture while at the same time allowing me to focus on the smaller details.

Create CalculatorView

Create a new SwiftUI View and save it as CalculatorView

1*jT2Vlt6G0K0zQGCk5BGENQ
1*wIcbxXQOm7J ZI6g6A IhQ

We will be using CalculatorView as our main view, so go ahead and delete ContentView.

1* fJRq7dmplzsHQlgrvdDQA

In the CalculatorApp file, change the lineContentView() to CalculatorView()

We’ll start by creating the outermost VStack in its simplest form.

Add a VStack with a Spacer and a Text view. — We will worry about the ButtonPad later.

1*0SrgifyLL2q 2ELLRioC6g

We are trying to break the problem into smaller subproblems. For our next objective, we are going to design the displayText. First, change the background color.

Add .background(Color.black) modifier to the VStack.

1*p4ajeBVkSMQoELNsvvR7KA

You’ll see our view now contains a strange line. We’ll fix that but first, the background color is the same as our displayText, so we can no longer see it.

Add the following modifier under Text(“0”):

.foregroundColor(.white)
1*L6AD9TYHdjHVuwdfhg2LDA

The VStack is skinny because Text does not have the full-screen width. Let’s make the Text frame occupy the whole screen width and make sure that the text prioritizes its position to the right (.trailing = right, .leading = left).

Add the following modifier to Text:

.frame(maxWidth: .infinity, alignment: .trailing)
1*gdDqwmoK7ig4oNU6m59F6w

Perfect! We are getting close. The layout for the displayText is almost done. However, the font does not look anything like the final product. Let’s update the font.

Add the following modifier to Text:

.font(.system(size: 88, weight: .light))

Now, notice how in the real calculator app, the text shrinks when displaying large numbers? That’s another modifier called minimumScaleFactor. It shrinks the view if necessary, and you can set the limit of how much small the view can get.

1*npsvQWoREdvTK6GzZMkgrg

By default, if the Text gets to its width limit, it will automatically occupy the next line. We want to limit the displayText to 1 line so we can shrink the text. The lineLimit modifier lets us do exactly that.

Add the following modifiers to Text:

.lineLimit(1).minimumScaleFactor(0.2)
1*hPeT9wlt5IRBaX1Njuz2ZQ

Finally, add .padding() as the first modifier and we are finished with the displayText.

Important: Remember modifiers are applied in order, so the order does matter.

Here’s the complete example code up until now.

1*eYuCsl5bAYVA6Y5uI2 ifg
CalculatorView with finished displayText

We are now going to start creating the ButtonPad.

Going back to the deconstruction of our view, for the ButtonPad we have a VStack, which contains 5 HStack views which have 3 to 4 Button views.

1*S0WlpGA73f8SjCFWMySb4A

That gives us a total of 19 buttons. We are not going to explicitly code 19 unique buttons for the calculator. We would have to set a custom title, foreground color, background color, and width for each one! What a waste of time.

The solution? — Enums.

We’ll create a ButtonType enum to do 4 things.

  1. Differentiate one button from another. (duh…)
  2. Get its corresponding button text
  3. Get its corresponding foreground color
  4. Get its corresponding background color

Create a new Swift File named ButtonType.swift and add an enum named ButtonType. Add all the button types you see on the screen as possible cases.

Note: The cases .allClear and .clear occupy the same button position and show on screen as needed. Nevertheless, they are different types of buttons.

Now is a great time to start structuring our project.

Create a new group named Models and move ButtonType in. Create another group named Views and move CalculatorView in.

1*1YNlzzimF5EpTTC5XTt9tA

We now have all the ButtonType possible cases. However, we can do better. Let’s simplify by grouping some of these cases.

There are 10 cases for digits (.zero, .one, .two… .nine) and there are 4 cases for arithmetic operations (.addition, .subtraction, .multiplication and .division).

It’s a good idea to group these cases into their own enums as they share similarities in both functionality (ex: digits can be combined to create numbers) and design (ex: digits have a gray background).

Inside the Models group, create a new Swift File named Digit and another named ArithmeticOperation.

1*W3qnDIJH TnthWL8DPFBtA

For Digit create an enum and pass all the corresponding cases. We’ll make this enum of type Int as it will let us get its value by using its rawValue property.

Make it conform to the CaseIterable protocol so we can easily be able to get an ordered array of all cases using its property Digit.allCases.

Make it conform to the CustomStringConvertible. This protocol requires you to add description property. We will use this as our button text.

Here’s the code:

Similarly, create an ArithmeticOperation enum and add the following code.

By going back to our ButtonType enum, we can now encapsulate plenty of our cases.

Add digit and operation cases with their corresponding enum as an associated value.

We are almost done with our models!

Make ButtonType conform to the Hashable protocol. This protocol will let us iterate over an array of ButtonTypes later on.

Make it conform to the theCustomStringConvertible protocol here as well. We will use the description property to show the corresponding button text as stated earlier.

ButtonType enum with protocols

We now have a text description to show for all different ButtonTypes. However, we are missing a way to get corresponding background and foreground colors.

1*yNWIqYH7UI9ImLqDy5U0Hw

Add import SwiftUI at the top as we need to reference Color

Create two computed properties of type Color named backgroundColor and foregroundColor and return the corresponding color for its case.

ButtonType completed example

We are finally done with our ButtonType model! We now have all the necessary information to build our ButtonPad containing buttons with their corresponding text, background color, and foreground color.

When we start building our ButtonPad view, everything will make more sense.

Perfect, we have our ButtonType enum with all the calculator button types. The problem? — There’s no order yet.

In what order the buttons are displayed is the responsibility of the view, not the model. So let’s go back to our CalculatorView and add a new buttonTypes 2-dimensional array with the correct button type order.

A 2-dimensional array representing the rows and columns of the ButtonPad

Now that we have a 2-dimensional array with the ButtonType order, we are going to start Building the ButtonPad. We need to create a VStack with its corresponding HStack views and inside each Button.

Replace the // TODO: ButtonPad line with the following code

The CalculatorView now shows a grid of buttons in the correct order

Don’t worry about the button design, we’ll get right to it in a bit.

Your CalculatorView file should look something like this.

CalculatorView before refactor

The code is getting kind of dirty and we are far from done, so let’s do a little refactoring to make everything a little clearer.

Encapsulate displayText and buttonPad and move them into an extension a the bottom of theCalculatorView file

CalculatorView after refactor

Now the code is easier to understand. I like to encapsulate components this way when they start getting a little too complex.

As we can see, design-wise, the calculator buttons are looking abysmal. We’ll create a new CalculatorButtonStyle to fix that right away.

Create the CalculatorButtonStyle

Create a new group named Styles and add a new Swift File named CalculatorButtonStyle:

1*91PDKUIBRRmynW3QM srJQ

We will create a custom ButtonStyle for our calculator buttons. If you want to learn more about creating custom ButtonStyles, I already wrote a story, you can find it here.

For now, we need to set 3 properties for the style. — the button size, the foregroundColor, and the backgroundColor.

Add the following code:

If you’ll like to see a live Preview of the ButtonStyle, you can add this code on the bottom of the file.

1*uWaKekgQt5vrrpp1Co IIw
CalculatorButtonStyle preview

Now, let’s go back to our CalculatorView file, find our buttonPad property, and add the buttonStyle modifier with our newly created CalculatorButtonStyle. Pass the corresponding parameters. For now, use size: 80, we’ll calculate the correct button size at the end.

Add the CalculatorButtonStyle to the existing Button
1*8iKQodmrrgH2gPSVMb3jNw
Our CalculatorView up until now

We are almost done with our CalculatorView. We are just missing 2 important details.

  1. We need to calculate the button size (to get rid of the 80 constant).
  2. We need to make the “0” button wide.

Small Refactor — CalculatorButton

Before continuing with the final details, let’s do a little refactoring on our CalculatorView as we are going to be adding a little bit of extra complexity, it’s a good idea to maintain our code as clean as we can.

We are going to encapsulate the calculator button into its own struct.

Create a new group inside Views named Components. Inside, create a new SwiftUI View named CalculatorButton.

1*4ItBT1Y AX0MYsLLSEFuOw

In larger-scale apps, it would be good practice to include all the components exclusively used by the view below, in our case, all components that are exclusively used by the CalculatorView.

This is a refactor, so no new functionality will be added. We are simply moving code around to make it cleaner.

Cut and paste the Button code from CalculatorView into the CalculatorButton body.

The same code previously written

Notice how I made an extension of CalculatorView and added the CalculatorButton struct inside? That’s good practice. CalculatorButton is not going to be used outside of CalculatorView so we can declare it inside.

Extra: If you want to use Previews for this view, add this code:

Finally, add our newCalculatorButton to the CalculatorView

Updated CalculatorView

No new functionality was added in this step, but our code is way cleaner.

Calculating the Button Size

Now, a little bit of math. To calculate the button size we are going to need the screen width, the button count, and the sum of spacings between each button and outer padding.

  • We can use UIScreen.main.bound.width to get the screen width. ✅
  • The button count is easy, there should be 4 buttons on each row. ✅
  • Getting the spacings and paddings might get a little bit trickier. ❓

Let’s set constant spacings and paddings.

We have something called implicit spacings right now. We have not explicitly set the spacing amount in our buttonPad stack views. We also have no padding set on the outside of the CalculatorView, so our buttons technically extend to the edges of the screen.

Let’s first set a constant spacing/padding amount that we can access throughout our app.

Create a new Swift File name Constants.

1*3jB6cvqC8cEtZj8NKYa7cw

Import CoreGraphics (as we’ll reference CGFloat) and create a static padding property.

In larger apps, this struct is a lot more useful

We now have a constant, padding, we can use anywhere on our app.

Go back to CalculatorView.

Insert the .padding() modifier set to our Constants.padding property over the .background() modifier

Scroll down to the buttonPad property, set our Constants.padding to both the VStack and the HStack spacings

Now, let’s calculate the buttonSize.

Go to your CalculatorButton file and add the following function under body

Lastly, replace size: 80 with size: getButtonSize() in the buttonStyle parameters.

Final CalculatorButton code should look like this

CalculatorButton with calculated button size

Making the “0” Button Wide

Final detail. We can observe a difference in button shape with the “0” button over all the other buttons. As for now, our CalculatorButtonStyle can only set buttons to be circle shaped. — Let’s update that so we can give the “0” button its wide capsule shape.

Going back to our CalculatorButtonStyle, under foregroundColor, add the following property:

var isWide: Bool = false

Note: If isWide is not set, it will be set to false by default.

Go to makeBody and under the .frame() modifier add the following modifier:

.frame(maxWidth: isWide ? .infinity : size, alignment: .leading)

The final code for CalculatorButtonStyle should look like this:

1*W 0Kr2ztmB2WnU0wo85vSg
Now CalculatorButtonStyle allows wide buttons

Finally, go back to CalculatorButton and in the CalculatorButtonStyle initialization, just below foregroundColor, add the following line:

isWide: buttonType == .digit(.zero)

Your final CalculatorButton code should look like this:

Whew, that was long… Thanks for pulling through.

We are finished with the calculator view. Building views is always very time-consuming, especially when we stick to best practices without taking hacky shortcuts. Maintaining your views simple (code-wise) is essential for building larger apps, so always try to encapsulate when the views get a bit too complex.

1*mweEgvu2z5rfqWgKTLl7HQ
The final Calculator view

In the next tutorial, we are finally adding functionality to the Calculator. We’ll see how the models we created for ButtonType will facilitate us greatly in making the Calculator work. We’ll add the CalculatorViewModel and a Calculator class that will be in charge of computing everything.



News Credit

%d bloggers like this: