Sunday, February 24, 2008

Building a Cocoa Calculator Application with XCode 3 for Mac (Part 3)

The long awaited part 3 of my Cocoa Calculator series is complete and has arrived in high style, or should I say resolution? This is by far the longest in the series, coming in at a whopping 25 minutes. I am also providing the full source code so that you guys don't have to feverishly copy code from the video but can refer to it afterwards for reference if you need help building out your very own!

Edit (1/6/09): Thanks to one of my readers, we now have a secondary mirror site for downloading the video. Download locations: Mirror 1 Mirror 2

//
//  CalcModel.h
//  SimpleCalc
//
//  Created by Chris Ball (chris.m.ball@gmail.com) on 1/17/08.
//  Copyright 2008, Chris Ball, Strainthebrain.Blogspot.Com. 
//  All rights reserved.
//

#import <Cocoa/Cocoa.h>

@interface CalcModel : NSObject {
    float running_total;
    char  sign_state;
    bool  first_call;
}

//Establishing some public-facing properties.
@property(readwrite) float running_total;
@property(readwrite) char  sign_state;
@property(readwrite) bool  first_call;

//Used for computing and maintaing the model data.
- (void) computeNewDisplayVal: (float) currDisplayVal;

@end


//
//  CalcModel.m
//  SimpleCalc
//
//  Created by Chris Ball (chris.m.ball@gmail.com) on 1/17/08.
//  Copyright 2008, Chris Ball, Strainthebrain.Blogspot.Com. 
//  All rights reserved.
//

#import "CalcModel.h"

@implementation CalcModel

//Implicity declaring the setters/getters for our properties.
@synthesize running_total, sign_state, first_call;

- (void) computeNewDisplayVal: (float) currDisplayVal {
    if (self.first_call) {
        self.running_total = currDisplayVal;
        self.first_call = NO;
    }
    else {
        switch (self.sign_state) {
            case 'a':
                self.running_total = self.running_total + currDisplayVal;
                break;
            case 'd':
                self.running_total = self.running_total / currDisplayVal;
                break;
            case 'm':
                self.running_total = self.running_total * currDisplayVal;
                break;
            case 's':
                self.running_total = self.running_total - currDisplayVal;
                break;
        }
    }
}

@end


//
//  CalcController.h
//  SimpleCalc
//
//  Created by Chris Ball (chris.m.ball@gmail.com) on 1/17/08.
//  Copyright 2008, Chris Ball, Strainthebrain.Blogspot.Com. 
//  All rights reserved.
//

#import <Cocoa/Cocoa.h>
#import "CalcModel.h"

@interface CalcController : NSObject {
    IBOutlet id calc_display;
    IBOutlet id a_button;
    IBOutlet id s_button;
    IBOutlet id m_button;
    IBOutlet id d_button;
    CalcModel*    calc_model;
}

//Mathematical button events.
- (IBAction) push_add:        (id) sender;
- (IBAction) push_divide:    (id) sender;
- (IBAction) push_multiply:    (id) sender;
- (IBAction) push_subtract:    (id) sender;

//Numeric button events.
- (IBAction) push_zero:        (id) sender;
- (IBAction) push_one:        (id) sender;
- (IBAction) push_two:        (id) sender;
- (IBAction) push_three:    (id) sender;
- (IBAction) push_four:        (id) sender;
- (IBAction) push_five:        (id) sender;
- (IBAction) push_six:        (id) sender;
- (IBAction) push_seven:    (id) sender;
- (IBAction) push_eight:    (id) sender;
- (IBAction) push_nine:        (id) sender;

//Internal functions.
- (void)     push_number:    (id) sender;
- (BOOL)     sign_pushed;
- (void)     check_calc_model;

//Other button events.
- (IBAction) push_clear:    (id) sender;
- (IBAction) push_decimal:    (id) sender;
- (IBAction) push_equal:    (id) sender;

@end


//
//  CalcController.m
//  SimpleCalc
//
//  Created by Chris Ball (chris.m.ball@gmail.com) on 1/17/08.
//  Copyright 2008, Chris Ball, Strainthebrain.Blogspot.Com. 
//  All rights reserved.
//

#import "CalcController.h"
#import <RegexKit/RegexKit.h>

@implementation CalcController

- (IBAction) push_add:        (id) sender {
    if ([m_button state] == NSOnState || [s_button state] == NSOnState || [d_button state] == NSOnState) {
        [a_button setState:NSOffState];
    }
    else {
        [self check_calc_model];
        
        if (!calc_model.first_call)
            [self push_equal:0];
        
        calc_model.sign_state = 'a';
        [calc_model computeNewDisplayVal:[calc_display floatValue]];
        [calc_display setFloatValue:calc_model.running_total];
    }
}
- (IBAction) push_divide:    (id) sender {
    if ([a_button state] == NSOnState || [s_button state] == NSOnState || [m_button state] == NSOnState) {
        [d_button setState:NSOffState];
    }
    else {
        [self check_calc_model];
        
        if (!calc_model.first_call)
            [self push_equal:0];

        calc_model.sign_state = 'd';
        [calc_model computeNewDisplayVal:[calc_display floatValue]];
        [calc_display setFloatValue:calc_model.running_total];
    }
}
- (IBAction) push_multiply:    (id) sender {
    if ([a_button state] == NSOnState || [s_button state] == NSOnState || [d_button state] == NSOnState)
        [m_button setState:NSOffState];
    else {
        [self check_calc_model];

        if (!calc_model.first_call)
            [self push_equal:0];

        calc_model.sign_state = 'm';
        [calc_model computeNewDisplayVal:[calc_display floatValue]];
        [calc_display setFloatValue:calc_model.running_total];
    }
}
- (IBAction) push_subtract:    (id) sender {
    if ([a_button state] == NSOnState || [m_button state] == NSOnState || [d_button state] == NSOnState)
        [s_button setState:NSOffState];
    else {
        [self check_calc_model];

        if (!calc_model.first_call)
            [self push_equal:0];

        calc_model.sign_state = 's';
        [calc_model computeNewDisplayVal:[calc_display floatValue]];
        [calc_display setFloatValue:calc_model.running_total];
    }
}

- (IBAction) push_zero:        (id) sender {
    [self push_number:sender];
}
- (IBAction) push_one:        (id) sender {
    [self push_number:sender];
}
- (IBAction) push_two:        (id) sender {
    [self push_number:sender];
}
- (IBAction) push_three:    (id) sender {
    [self push_number:sender];
}
- (IBAction) push_four:        (id) sender {
    [self push_number:sender];
}
- (IBAction) push_five:        (id) sender {
    [self push_number:sender];
}
- (IBAction) push_six:        (id) sender {
    [self push_number:sender];
}
- (IBAction) push_seven:    (id) sender {
    [self push_number:sender];
}
- (IBAction) push_eight:    (id) sender {
    [self push_number:sender];
}
- (IBAction) push_nine:        (id) sender {
    [self push_number:sender];
}

- (void)     push_number:    (id) sender {
    if ([self sign_pushed]) {
        [a_button setState:0];
        [d_button setState:0];
        [m_button setState:0];
        [s_button setState:0];
        [calc_display setStringValue:@""];
    }
    
    if ([[calc_display stringValue] compare:@"0"] && !calc_model.first_call)
        [calc_display setStringValue:[[calc_display stringValue] stringByAppendingString:[sender title]]];
    else 
        [calc_display setStringValue:[sender title]];
}
- (BOOL)     sign_pushed {
    if ([a_button state] == NSOnState || [s_button state] == NSOnState || [m_button state] == NSOnState || [d_button state] == NSOnState)
        return YES;
    else return NO;
}
- (void)     check_calc_model {
    @try {
        if (calc_model == nil) {
            calc_model = [[CalcModel alloc] init];
            calc_model.first_call = YES;
        }
    }
    @catch (NSException *ex) {
        NSLog(@"check_calc_model: Caught %@: %@", [ex name], [ex reason]);
    }
}

- (IBAction) push_clear:    (id) sender {
    if (calc_model != nil) {
        [calc_model release];
        calc_model = nil;
    }
    
    [calc_display setIntValue:0];
    
    if ([self sign_pushed]) {
        [a_button setState:0];
        [d_button setState:0];
        [m_button setState:0];
        [s_button setState:0];
    }
}
- (IBAction) push_decimal:    (id) sender {
    if ([self sign_pushed]) {
        [a_button setState:0];
        [d_button setState:0];
        [m_button setState:0];
        [s_button setState:0];
        [calc_display setStringValue:@""];
    }
    
    if (![[calc_display stringValue] isMatchedByRegex:@"[.]"])
        [calc_display setStringValue:[[calc_display stringValue] stringByAppendingString:@"."]];
}
- (IBAction) push_equal:    (id) sender {
    [self check_calc_model];
    [calc_model computeNewDisplayVal:[calc_display floatValue]];
    [calc_display setFloatValue:calc_model.running_total];
    calc_model.first_call = YES;
}

@end