Week 8 - Fraction Calculator

Goals

Preparation

One Button—(a pre-calculator?)

Application delegate

Starting in Xcode version 8.3, Apple are strongly emphasising use of "Storyboards" to create GUI applicattions. Indeed without warning they removed the option not to use storyboards! However we will defy Apple and start by building our GUI apps without using storyboards. Why? Because using storyboards requires you to understand the role of a number of AppKit classes that we have not discussed yet in lectures. We will soon cover those classes. Also, you will find plenty of code on the web that will provide Xcode projects that do not use storyboards.

Because Apple has removed the option to start a new project in Xcode that doesn't use storyboards, we will instead begin by requesting that you decompress this archived Xcode project. It will decompress files into a folder "FractionCalculator" that itself includes "FractionCalculator.xcodeproj" that you can double-click in Finder, and then hack away. (It is possible to create a new project that uses storyboards and then remove the storyboards, but the steps are likely to appear unhelpfully mysterious.)

So, open your copy of the FractionCalculator.xcodeproj project in Xcode. Instead of the usual Command Line Tool, this project is instead a Cocoa Application. In your first attempt you will not create the entire calculator. Initially, your application will be a one button calculator. We have named the application "FractionCalculator" (by convention, Cocoa applications start with a capital letter, while command line applications start with a lowercase letter).

In the Project navigator, you should see file called "AppDelegate.swift". This file will be created for you by Xcode and will contain the implementatino of your application delegate class. An application delegate is a class that conforms to the NSApplicationDelegate protocol, and it's the means of plugging in your code into Cocoa's application framework. The framework provides a generic, fully functional, application that, among other things, can display windows and react to user's input. You customise the behaviour of your application by creating a class that is application's delegate. The framework will invoke delegate's methods at various stages/events of the application's execution. Xcode has decided that your delegate will be a class called AppDelegate and it will be at least populated with two empty methods. As the names of these methods suggest, one is invoked on application start, the other just before the application terminates. You don't need to create an instance of the AppDelegate, as this is done automatically when application starts. All you need to do is to implement the AppDelegate class to implement your program.

Application window

Note AppDelegate's stored property named "window", whose declaration is preceded by type @IBOutlet keyword. It's a reference to NSWindow window object, which is the main window used by your application. To see that window, in the Project navigator select file "MainMenu.xib". XIB files store information about all the GUI elements in your application. You don't edit this file directly. Instead you specify the graphical layout of your application through the Interface Builder. Xcode saves information about the elements in the window, their properties, position, etc. into the XIB file. By default, an application is configured with a single, empty window. To see it in the Interface Builder, select the window drop-down, as shown in the screen capture below.

The window is already connected to your AppDelegate through that @IBOutlet construct (outlets will be explained in more detail in the next section). Build and run the application (the same way your ran your command-line programs - by clicking on the play button in Xcode toolbar). At this point, it will be just an empty window, but the application is fully functional. For instance, you can quit the application. From menu select FractionCalculator -> Quit FractionCalculator - the application will close.

GUI elements

Time to add some GUI elements to the window. Back in Xcode, make sure you can see the main window in the Interface Builder. On the right-hand side of the Interface Builder, in the lower part of the Utilities area, there's a tool bar with four icons. Select the Object library icon (follow the arrows in the diagram below if you have trouble finding it). Object library will display a list of all available GUI elements. At the bottom there is a search box, where you can Search for object.... Type "label" into the search, and the list of objects should get filtered. Select the "Label" object and drag it onto the window in the Interface Builder. Resize the label in your window so that its position and size corresponds more or less to the diagram below.

There's another toolbar on the upper part of the Utilities area. Select the Attributes Inspector icon there, , to view the attributes of the object currently selected in the Interface Builder. Make sure that the label you just placed in the window is selected. Change the "Alignment" property of the label to right-align, check the "Draws Background" in the "Display", set "Text Color" and "Background" to whatever you like (I chose white and blue respectively), and click on the icon in the "Font" box to increase the label's font size.

Next, you will add a button to your window. In the Object library part of the Utility area search for "button". Drag the "Push Button" element onto your window - you can resize it as shown in the diagram below. With the button element selected in the Interface Builder, you can change its properties in the Attributes Inspector - set "Style" to "Square", set "Title" to "0" and increase the "Font" size.

You may also need to change an attribute of the main window itself. Selecting main window in the Interface Builder can be tricky. Entire area of the window is covered by the "View" element, which often gets selected when you click on the window. The easiest way to select what you want is to get Xcode to show you the hierarchy of all the GUI elements in the window. In the bottom left corner of the Interface Builder there is a Show/hide document outline icon - toggle it to see the hierarchy of your GUI elements. At the moment, that hierarchy should be as the one shown in the diagram below. Select FractionCalculator window (not the view directly below it). Now, in the Attributes Inspector you can see various properties of the window. Make sure that the the "Resize" attribute is unchecked - the window will be of fixed size.

Run your application again. It should now be a window with a label and a button. You can click the button but nothing will happen, as you haven't specified its behaviour yet.

Connecting the GUI elements

In this part of the exercise you will provide the code that determines how your text field and button will behave when the application runs. In this application, the button will be the “0” button on the calculator and the text field will show a “0” when user hits the button. The easiest way to do this is to have the Interface Builder and the source code of AppDelegate displayed side by side. Click on the "Show the Assistant Editor" icon . A new window will appear, and it should show the source code of "AppDelegate.swift". Assistant editor automatically decides what to display - when MainMenu.xib is selected in the Project navigator, the assistant will usually display the source code of the application's delegate. You can always force it to display something else (by changing its automatic display setting). However, at the moment it should be showing exactly what you need to see.

Right now, the AppDelegate class has a single stored property, "window", which is a weak reference to an NSWindow object. Its declaration is preceded by @IBOutlet keyword. In Cocoa, outlets are references to GUI elements, which are not instantiated by the developer (you), but by the XIB file. The XIB file contains all the information about all the GUI objects. You are customising the XIB file whenever you're changing things in the Interface Builder. For instance, when you added the label and button object to the window, XIB file recorded the fact that these objects need to be instantiated on startup. The @IBOutlet keyword is an Interface Builder keyword. It's ignored by the compiler, but it does let Interface Builder know that the variable references an object from the XIB file. You can tell that the "window" outlet is connected, because there's a filled-in circle (connection) to the left of the property, on the margin. An unconnected outlet would be marked as a hollow circle. By default, the application delegate is setup with a connected outlet to the main window.

Implicitly unwrapped optionals

All weak references must be optionals, because in theory they may reference nothing. However, sometimes the program logic assures that these references are never actually nil. Weak reference outlets, that are connected to GUI elements, get initialised at application startup. This happens before application delegate gets to run, and so, from delegate's point of view, these outlets will always be initialised. It would be a lot of hassle to keep unwrapping the reference every time it needs to be used. For this reason Swift provides implicitly unwrapped optionals, declared by following the reference type with the "!" operator. That means that the corresponding variable already references the result of the unwrapping of the optional, and so there is no need to unwrap it.

In order to modify the contest of the label in your window, you will need an outlet that references it. The easiest way to create a connected outlet to your GUI element is to Control-click (that is click while pressing the Control key) that element in the Interface Builder and drag the mouse to the Assistant Editor into the source code where the reference is to be created (as shown in the diagram below).

When an "Insert Outlet or Action" message appears, let go of the mouse. You should see a dialog box. Make sure the "Connection" says "Outlet". The "Name" specifies the name of your new property - set it to "display". Other options should be left as they are: "Type" should be set to "NSTextField" and "Storage" should be set to "weak". Click the Connect button. A new connected outlet reference should appear in your class. You don't need to instantiate it, because it's connected to a GUI element - instantiation will happen on application startup.

To define what happens on click of the button, you will need to create an action method. Whereas outlets are references, actions are methods that are associated with GUI elements. Again, the easiest way to create such a method is to Control-click on the corresponding GUI element (the button) and to drag the mouse over to where the method is to be declared (as shown in the diagram below).

Again, when "Insert Outlet or Action" dialog appears, let go of the mouse. This time, change the "Connection" to "Action". The "Name" is the name of your method: call it "clickDigit". Change "Type" to "NSButton" and click Connect. A new method should be created in AppDelegate as shown in the diagram below.

The new method is preceded by the @IBAction keyword. This is again a keyword used by the Interface Builder (ignored by the compiler) that signifies that the method is a callback associated with a GUI object. Note the filled circle next to the newly created method. Once again, you don't have to explicitly setup the callback - it's all somewhere in the XIB file. From now on, whenever the button is clicked, the "clickDigit" method of AppDelegate is going to be invoked.

Now, in the AppDelegate, you have a reference to the label object and a method that will be invoked when the button is clicked. All that remains is to specify what happens to the label when "clickDigit" is invoked. An NSTextField object has a property "stringValue", which specifies the text displayed in its box. Update the contents of the "clickDigit" method, so that "display" object's "stringValue" is set to "0". Your AppDelegate should contain the following code:

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!
    @IBOutlet weak var display: NSTextField!

    func applicationDidFinishLaunching(_ aNotification: Notification){
        // Insert code here to initialize your application
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
    @IBAction func clickDigit(_ sender: NSButton) {
        display.stringValue = "0"
    }
}

Run the application. Now, when you click the button, the label should display "0".

Fraction Calculator

In this part of the exercise, you will implement the entire fraction calculator application.

Model-View-Controller

Grab "Parser.swift" from Lab 5 along with your implementation of "Fraction.swift". Add them to your FractionCalculator project. These two classes will constitute the Model of the application. This model is capable of interpreting a string expression as a set of mathematical operations, and returning a Fraction result. All that the Controller, AppDelegate, needs to do now, is to collect information from the View classes (the GUI elements), build the string describing the expression specified by the user, and pass it to the model for evaluation.

Create the buttons for digits 1-9. The easiest way to do this is by copying (Control-C) and pasting (Control-V) the existing 0 button in the Interface Builder. The pasted buttons maintain the connection to the copied button's action. You can easily create 9 new buttons. Change their titles to different digits.

To verify that all the buttons are connected to the "clickDigit" action, mouseover the connect circle next to the method - connected buttons should be indicated in the Interface Builder as shown in the figure below.

Given that "clickDigit" is going to be invoked for every digit button, you need some extra information coming in, which specifies which button was clicked. Buttons have a "Tag" property. In the "Attributes Inspector" set the "Tag" attribute of each of the digit buttons to its digit value.

Change the contents of "clickDigit" to read the "tag" property of the "sender" and add it as a string to "display"'s "stringValue". The "sender" argument references the button object that was clicked.

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!
    @IBOutlet weak var display: NSTextField!

    func applicationDidFinishLaunching(_ aNotification: Notification){
        // Insert code here to initialize your application
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
    @IBAction func clickDigit(_ sender: NSButton) {
        let digit: Int = sender.tag
        display.stringValue += "\(digit)"
    }
}

Run the application and test the buttons.

More buttons

Add more buttons to your calculator. This time don't copy and paste from the digits - you don't want the new buttons to be connected to the "clickDigit" method. If you drag a new square button from the object library, you can then copy and paste that button to create six new ones. Change their titles to "C" (for clear), "+", "-", "x", "/" and "=". A possible layout is shown in the diagram below.

This time you will create the action methods first, and then connect them to appropriate buttons. Add the following code to AppDelegate.

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!
    @IBOutlet weak var display: NSTextField!

    func applicationDidFinishLaunching(_ aNotification: Notification){
        // Insert code here to initialize your application
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
    @IBAction func clickDigit(_ sender: NSButton) {
        let digit: Int = sender.tag
        display.stringValue += "\(digit)"
    }
    
    @IBAction func clickAdd(sender: NSButton) {
        display.stringValue += "+"
    }
    
    @IBAction func clickSubtract(sender: NSButton) {
        display.stringValue += "-"
    }
    
    @IBAction func clickMultiply(sender: NSButton) {
        display.stringValue += "*"
    }
    
    @IBAction func clickDivide(sender: NSButton) {
        display.stringValue += "/"
    }
    
    @IBAction func clickClear(sender: NSButton) {
        display.stringValue = ""
    }
    
}

The new methods define actions for five buttons. The methods for addition, subtraction, multiplication and division just append appropriate mathematical operation character to "display"'s "stringValue". The clear method clears that "stringValue". The @IBAction keyword prompts Xcode to create hollow circles on the left-hand margin next to each method. This means that actions are not connected to GUI elements. To connect an action, click on its connection circle and drag the mouse to a button.

Connect all five actions to appropriate buttons. Once you're done, all the connection circles should be filled-in.

Run the application and verify that the connected buttons work.

The last button to connect is the "=" button, which will trigger evaluation of the expression from label's "stringValue". Add the following action method to AppDelegate.

01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!
    @IBOutlet weak var display: NSTextField!

    func applicationDidFinishLaunching(_ aNotification: Notification){
        // Insert code here to initialize your application
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }
    @IBAction func clickDigit(_ sender: NSButton) {
        let digit: Int = sender.tag
        display.stringValue += "\(digit)"
    }
    
    @IBAction func clickAdd(sender: NSButton) {
        display.stringValue += "+"
    }
    
    @IBAction func clickSubtract(sender: NSButton) {
        display.stringValue += "-"
    }
    
    @IBAction func clickMultiply(sender: NSButton) {
        display.stringValue += "*"
    }
    
    @IBAction func clickDivide(sender: NSButton) {
        display.stringValue += "/"
    }
    
    @IBAction func clickClear(sender: NSButton) {
        display.stringValue = ""
    }
    
    @IBAction func clickEquals(sender: NSButton) {
        if let result = Parser<Fraction>.evaluate(display.stringValue) {
            display.stringValue += "=\(result)"
        } else {
            display.stringValue = "Error"
        }
    }
}

Connect the new action to the "=" button.

Run and test the application. Your output may differ, depending on your implementation of Fraction class, but the value of the result should be the same.

This implementation has some weaknesses - the interface allows the user to create invalid expressions, which will evaluate to "Error". Pressing the "=" button more than once will also create an "Error". Try to think of possible changes to AppDelegate to improve usability.