Lab 2 : Calculator

Objective
  • To use many of the Swift features we’ve covered, including:
    • functions and properties (instance variables)
    • computed vs. stored properties
    • let versus var
    • enum, struct, and class
    • Optionals
    • String and Dictionary
    • ?? operator (hopefully)
  • To gain experience using XCode’s GUI design features, and to implement an iOS app from scratch using the following basic concepts:
  • To create an iOS project from scratch in XCode.

Table Of Contents

Partners

You may work with a partner on this lab if you like. To do so, set up one of your Gitlab projects to be shared with both partners. Details on doing that can be found it Part 6 of the GitLab Tutorial.

Overview

Build a calculator like the one we designed in lecture. It should be designed for an iPhone, and look good and work properly in both portrait and landscape modes. Your design should follow the MVC pattern, with each piece designed and tested as independently as possible. The following outline is one way to approach building your app.

Problem 0: Project Setup

  1. Clone your lab2 repository following the same steps as previous weeks. The repository is initially empty (except for a .gitignore file, which tells git to ignore temporary files you should not track with version control).

  2. To create a new project, first launch XCode.

  3. Select “File -> New -> Project…” from the menu bar.

  4. Select “iOS” from the list at the top of the dialog box, and then “Single View App”. The click Next.

  5. Set the Product Name to “Calculator” and the Language to “Swift”. Enter your name as the Organization Name, and edu.williams.cs as the Organization Identifier. Be sure that the “Include Unit Tests” box is checked. The window should something like the following. Verify its settings and then click Next.

  1. Select the folder for your lab2 repository and then click Create.

  2. Make your Schemes shared, by clicking on the Scheme pop-up menu in the Toolbar and selecting “Manage Schemes…”. Then click the “Shared” check box for each scheme in the table that appears in the dialog box, and then click “Close”.

  3. As usual, move a few files out of the way. In the Project Navigator, select AppDelegate.swift, LaunchScreen.storyboard, and Assets.xcassets by command-clicking on each. Then control-click on them, and select “New Group From Selection”. Rename the newly-created group “Support Files”.

  4. All projects should include a README.md file. Create one now by selection “File -> New -> File…” from the menu bar, and then select “Markdown File” as the template for the new file. Save the new file in the top level directory for your project, as shown below: Include in the README your names and a brief description of your project. For this week, one or two sentences is sufficient.

  5. At this point, you are ready to go. However, I suggest adding and comitting all the project files to GitHub before doing anything else. If you are in the top-level directory of your repository, git add Calculator followed by git commit -a should suffice.

  6. Run your Calculator and verify that it compiles and runs on an iPhone 8/X/XR or similar phone. It won’t do anything interesting, but you can verify that the simulator properly starts and launches your application, which will just display a white screen. The first time you run your code, XCode must essentially create a new phone installation to use in the simulator. This may take a minute or two, but on consecutive runs it will be much faster.

Problem 1: CalculatorModel

Create a new CalculatorModel class for our model.

  1. Select the “Calculator” folder in the Project Navigator (that is, the folder containing Main.storyboard, etc.).

  2. Select “File -> New -> File…” from the menu bar.

  3. Select “Swift File” for the Template and click Next.

  4. Name your class CalculatorModel.swift. Be sure it will be stored in the “Calculator” folder, that its Group is “Calculator” and the “Calculator” Target is checked. The window should like the following. Verify its settings and then click Create. Once created, if the new file does not appear next to the other source files within the Calculator group in the Project Navigator, you can just click and drag it to the right place.

  1. You may use our template as a starting point if you wish.

A few details on this part:

  • You may wish to complete all (or most) of the model – including unit testing (below) – before proceeding to work on the UI.

  • You can use any Unicode characters you want as your mathematical symbols for your constants and operations. For example, symbols like 𝝅, √, and ± are perfectly fine.

  • In your case to perform a binary operation, you may need to complete the last binary operation that was started so that, for example, the sequence “6 x 5 x 4 =” properly results in 120).

And be sure to unit test thoroughly.

Testing

Include unit tests for your CalculatorModel class. You should have a folder titled “Calculator Tests” in your project. You can run the tests by changing the Run Button to be the Test Button as we did last week. But the keystroke “Command-U” will also run the tests. If you look in that folder, there will be a file CalculatorTests.swift. You can delete the skeleton text XCode inserted in that file and write your own unit tests in their place. Here’s one example test to get you started. Be sure to give each test a descriptive name that always starts with test..., and to include the @testable import line. If XCode complains that Calculator is undefined when you try to compile and run the tests, you most likely forgot that line. Handy tip: Now that we are building iOS apps, you can use Command-R to run and Command-U to run tests without switching the play button from Run to Test and vice-versa.

import XCTest

// allow your test cases to access internal 
// properties/methods of the Calculator module.
@testable import Calculator

class CalculatorTests: XCTestCase { 
  
    func testExample() {
      let c = CalculatorModel()
      c.set(operand: 10.0)
      c.perform(op: "+")
      c.set(operand: 11.0)
      c.perform(op: "=")
      XCTAssertEqual(c.result, 21)
    }
}

Problem 2: View and CalculatorViewController

Design the View in your app’s Main.storyboard. Your calculator view should have at least 20 buttons. In addition to those from lecture — 0-9,+,*,/,-,=, and π — these should include buttons for at least a couple additional functions of your choice, as well as . (to enter floating points) and C (clear).

Once your calculator is laid out as you would like, create your custom CalculatorViewController subclass of UIViewController, and change the controller for you storyboard to that custom view controller in the Identity Inspector:

  1. Create the file as you did for the model above except select “Cocoa Touch Class” for the Template. — we use this whenever we create a custom version of a UIKit library class — and then select UIViewController in the “subclass of:” popup menu. Name your class CalculatorViewController.

  2. Open Main.storyboard and select the Controller for your scene. The easiest way to select items from a storyboard is often to use the Document outline appearing to the left side of the storyboard, as shown below. If the outline is not there, select “Editor -> Show Document Outline” from the menu bar. Then click on the controller line at the top, which I have highlighted. (Your controller will likely have a different name than what is showing.)

  1. Then click on the “Identity” button at the top of the Inspector panel on the right-hand side of XCode’s window. That button is also highlighted in the image. Below that button, change the Class to be CalculatorViewController.

  2. At this point the ViewController.swift file in your project is no longer needed. You can control click on it and delete it.

  3. You can use our template as a starting point for your controller. Be sure to connect your buttons to the appropriate event handlers. I have given you one @IBAction handler for digits. You’ll need to wire that one to your UI button and also create a second action for the command buttons. Also, note that MARK comments in the template. These help you organize and navigate through a file with the “Jump Bar” at the top of the editor:

    If you click on CalculatorViewController in the Jump Bar, a popup menu will allow you to jump to any declaration in that file. The MARKs appear as headings in that popup menu.

Proceed to fill in the details to process touches and update the display as appropriate. A few additional details for this part:

  • Use color to make your UI look nice. At the very least, your operations buttons must be a different color than your keypad buttons, but otherwise you can use color in whatever way you think looks nice.

  • Your layout constraints should ensure that the layout looks okay in either portrait or landscape mode.

  • Allow legal floating point numbers to be entered using the . button in your calculator. The String method contains(String) will be useful here. This should only take one or two lines of code. Be careful to test (and nicely handle) the case where the user starts off entering a new number by touching the decimal point (i.e. the user touches the decimal point button while userIsInTheMiddleOfTyping is false).

  • Implement the clear button C to reset the state of the calculator. This should leave your Calculator in essentially the same state it was in when you launched it.

  • If you set a UILabel’s text to nil or an empty string, it will resize to have zero height (shifting the rest of your UI around accordingly). You may find this disconcerting for your users. If you want a UILabel to appear empty, but not be zero height, simply set its text to be a single space character: “ ”.

Problem 3: Description Label

Extend the calculator to show a description of the sequence of operands and operations that led to the currently displayed result value. To do this:

  • Add a Bool property to your model called resultIsPending that returns whether there is a binary operation pending.

  • Add a String property to your model called description that returns a description of the sequence of operands and operations that led to the value returned by result (or the result so far if resultIsPending). The character “=” should never appear in this description, nor should “...”. We give some hints on this step below.

  • Implement a UILabel descriptionLabel to your UI showing the sequence of operands and operations that led to (or is leading to if resultIsPending) what is (or “will be” if resultIsPending) showing in the display. If resultIsPending is true, put ... on the end of the UILabel, else put =. If the userIsInTheMiddleOfTyping, you can leave the descriptionLabel showing whatever was there before the user started typing the number. Be sure your Clear button clears the history as well.

Here are some examples of the completed feature in action.

Touches Description UILabel Display UILabel
7 + “7.0 + …” 7.0
7 + 9 “7.0 + …” 9.0
7 + 9 = “7.0 + 9.0 =” 16.0
7 + 9 = √ “√(7.0 + 9.0) =” 4.0
7 + 9 = √ + 2 = “√(7.0 + 9.0) + 2.0 =” 6.0
7 + 9 √ “7.0 + …” 3.0
7 + 9 √ = “7.0 + √(9.0) =” 10.0
7 + 9 = + 6 = + 3 =      “7.0 + 9.0 + 6.0 + 3.0 =” 25.0
7 + 9 = √ 6 + 3 = “6.0 + 3.0 =” 9.0
5 + 6 = 7 3 “5.0 + 6.0 =” 73.0
4 × π = “4.0 × π =”    12.5663706143592
 
Hints For Part 3
  1. CalculatorModel Abstraction. For Parts 1 and 2, your model’s abstract state was the single public property, namely result. In Part 3, you can think of the CalculatorModel as an abstraction of three items that can be observed by the client through the analogous public properties:

    Abstract State
    result The value currently stored in the calculator’s accumulator.
    resultIsPending whether or not the calculator is waiting for the second operand of a binary operation.
    description The history of steps leading to the current calculator state, represented by a String.

    Note: there are probably better abstractions than this. For example, we could represent the history as a sequence that could be traversed and examined rather than just as a String, or we could include which binary operation may be pending rather than indicating that there is some pending binary operation. However, this less detailed abstraction is sufficient for our purposes this week.

  2. Implementation. In the simplest case, the description property should just produce a string describing how the current accumulator value was created. If you think of it this way, then all you have to do is to create and store a String representation for the accumulator every time you set the value of the accumulator to a new value. Thus, you may find yourself wanting your model to have two variables (the accumulator and its String representation) whose values are always set at the same time and that should never be out of sync with each other. In this case, a good design is to capture both variables in an immutable struct:

    private struct ValueWithDescription {
      let value : Double
      let description : String
    }

    Your accumulator’s type becomes ValueWithDescription, and your model’s result and description properties are computed based on the accumulator’s value.

    Things are a bit trickier when you have a pending binary operation: the calculator’s description should not only include how the accumulator was created, but also a description of how the pending binary operation was created (ie, the description of how the left operand was computed, and by the operator symbol). While you’ll likely want to make other changes to PendingBinaryOperation struct as well, an important first step to supporting this is to have it store and operate on ValueWithDescription operands rather than on Double operands to facilitate doing that:

    private struct PendingBinaryOperation {
      let leftOperand : ValueWithDescription
      let binaryOpFunction : (Double,Double) -> Double
    
      func apply(rightOperand : ValueWithDescription) -> ValueWithDescription { ... }
    }

    You may want to store the operator symbol in the structure as well.

  3. Abstraction Function. We haven’t had much practice writing abstraction functions yet, but here is one way that we could describe mapping the accumulator and pendingBinaryOperation representation properties to the model’s abstract state used by clients:

    Abstraction Function
    AF(self):
    self.resultIsPending  \(\equiv\) self.pendingBinaryOperation != nil
    self.result  \(\equiv\) self.accumulator’s value, or nil if it does not exist
    self.description  \(\equiv\) self.accumulator’s description if no result is pending; or
    self.pendingBinaryOperation’s description followed by self.accumulator’s description if result is pending
  4. Testing. Test the changes to your model as you go, perhaps even before modifying the UI.

    Testing

    Add unit tests for CalculatorModel to test the resultIsPending and description properties. The examples in the table above should provide the basis for a number of tests, but note that you won’t be able to test the scenarios where the user is in the middle of typing a multi-digit number since the CalculatorModel interface doesn’t consider partially entered operands. Here are a couple tests to get you started:

    func testPendingBinaryOp() {
      let c = CalculatorModel()
      c.set(operand: 7.0)
      c.apply(operator: "+")
      XCTAssertEqual(c.description, "7.0+")
      XCTAssertTrue(c.resultIsPending)
    }
    
    func testBinaryOpCompletedWithEqual() {
      let c = CalculatorModel()
      c.set(operand: 7.0)
      c.apply(operator: "+")
      c.set(operand: 9.0)
      c.apply(operator: "=")
      XCTAssertEqual(c.result, 16)
      XCTAssertEqual(c.description, "7.0+9.0")
      XCTAssertFalse(c.resultIsPending)
    }

Extras

Here are some thoughts on additional features you could add:

  1. Implement a “backspace” button for the user to touch if they hit the wrong digit button. This is not intended to be “undo,” so if the user hits the wrong operation button, he or she is out of luck! It is up to you to decide how to handle the case where the user backspaces away the entire number they are in the middle of typing. You will probably find the Strings and Characters section of the Swift Reference Guide to be helpful here.

  2. Use the iOS struct NumberFormatter to format your display so that it only shows 6 digits after the decimal point (instead of showing all digits that can be represented in a Double). This will eliminate (or at least reduce) the need for Autoshrink in your display. While you’re at it, make it so that numbers that are integers don’t have an unnecessary “.0” attached to them (e.g. show “4” rather than “4.0” as the result of the square root of sixteen). You can do all this for your description in the CalculatorModel as well.

  3. Make one of your operation buttons be “generate a random double floating point number between 0 and 1”. This operation button is not a constant (since it changes each time you invoke it). Nor is it a unary operation (since it does not operate on anything). The Swift function drand48() will be useful here.

  4. Add app icons to your Assets.xcassets file (which you likely moved to Supporting Files). The only trick here is to provide versions of your icon in all the right sizes.

  5. Create a launch screen for your Calculator in LaunchScreen.storyboard. Remember that you will want to use some simple autolayout to make your launch screen look good on all platforms.

Question

Add a brief description of any extras to your README.md file so we know what to look for.

What To Turn In

Be sure to submit everything in the following checklist:

Submission Checklist
  1. The code for the Calculator app and your unit tests.
  2. A description of any extra features you added, also in the README.md.

As always, verify your solution is complete and runs by cloning your repository to a temporary location and checking it.

Grading Guidelines

This class emphasizes both program correctness and best practices, including programming style, documentation, specification, sound design principles, testing methodology, etc. Grading will reflect both of these emphases. There is, of course, some subjectivity when evaluating design and specification decisions, but your choices should follow the basic philosophies and methodologies we have been exploring. Labs will be graded on the following scale:

A+: An absolutely fantastic submission of the sort that will only come along a few times during the semester. Such a submissions reflects substantial effort beyond the basic expectations of the assignment.

A: A submission that exceeds our standard expectation for the assignment. The program must reflect additional work beyond the requirements or get the job done in a particularly elegant way.

A−: A submission that satisfies all the requirements for the assignment — a job well done.

B+: A submission that meets the requirements for the assignment, possibly with a few small problems.

B: A submission that has problems serious enough to fall short of the requirements for the assignment.

C: A submission that has extremely serious problems, but nonetheless shows some effort and understanding.

D: A submission that shows little effort and does not represent passing work.