[iOS] 计算器仿写
前言
这个项目主要是锻炼对多种复杂情况的考虑问题,以及使用栈这种数据结构的熟练程度。下面我将以 MVC 模式的角度来去介绍这个项目。
UI 界面

上面的 UI 界面都采用了 Masonry 来布局,在这个界面有两个部分组成一个就是 button 按钮一个就是 textField。在这里的 button 按钮我就是通过 for 循环来进行布局的,然后我们还给了 tag 值来做了区分。
下面就是我的 UI 相关的代码。
objc
#import "CaculateView.h"
#import "Masonry.h"
@implementation CaculateView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
CGFloat SIZE = [UIScreen mainScreen].bounds.size.width / 5;
NSArray *grayArray = @[@"AC", @"(", @")"];
NSArray *orangeArray = @[@"×", @"÷", @"+", @"-", @"="];
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
_baseButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
_baseButton.layer.cornerRadius = SIZE / 2;
_baseButton.titleLabel.font = [UIFont systemFontOfSize:42];
_baseButton.tag = j + 4 + i * 4;
[self addSubview:_baseButton];
[_baseButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(self).offset(-(75 + (SIZE + 17) * (i + 1)));
make.left.equalTo(self).offset(5 + [UIScreen mainScreen].bounds.size.width / 4 * j);
make.width.mas_equalTo(SIZE);
make.height.mas_offset(SIZE);
}];
if (j < 3) {
if (i < 3) {
[_baseButton setBackgroundColor:[UIColor colorWithWhite:0.15 alpha:1]];
[_baseButton setTitle:[NSString stringWithFormat:@"%d", j * 1 + i * 3 + 1] forState:UIControlStateNormal];
[_baseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
} else {
[_baseButton setBackgroundColor:[UIColor lightGrayColor]];
[_baseButton setTitle:grayArray[j] forState:UIControlStateNormal];
[_baseButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
}
} else {
[_baseButton setBackgroundColor:[UIColor colorWithRed:0.9 green:0.58 blue:0 alpha:1]];
[_baseButton setTitle:orangeArray[i] forState:UIControlStateNormal];
[_baseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
}
if (j == 0 && i == 3) {
_baseButton.titleLabel.font = [UIFont systemFontOfSize:34];
}
}
}
_baseButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
_baseButton.layer.cornerRadius = SIZE / 2;
_baseButton.titleLabel.font = [UIFont systemFontOfSize:42];
_baseButton.tag = 20;
[self addSubview:_baseButton];
[_baseButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(self).offset(-SIZE);
make.left.equalTo(self).offset(5);
make.width.mas_equalTo(2 * SIZE + 20);
make.height.mas_equalTo(SIZE);
}];
[_baseButton setBackgroundColor:[UIColor colorWithWhite:0.15 alpha:1]];
[_baseButton setTitle:[NSString stringWithFormat:@"0"] forState:UIControlStateNormal];
[_baseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_baseButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
_baseButton.layer.cornerRadius = SIZE / 2;
_baseButton.titleLabel.font = [UIFont systemFontOfSize:42];
_baseButton.tag = 21;
[self addSubview:_baseButton];
[_baseButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(self).offset(-SIZE);
make.left.equalTo(self).offset(5 + [UIScreen mainScreen].bounds.size.width / 4 * 2);
make.width.mas_equalTo(SIZE);
make.height.mas_equalTo(SIZE);
}];
[_baseButton setBackgroundColor:[UIColor colorWithWhite:0.15 alpha:1]];
[_baseButton setTitle:[NSString stringWithFormat:@"."] forState:UIControlStateNormal];
[_baseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_baseButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
_baseButton.layer.cornerRadius = SIZE / 2;
_baseButton.titleLabel.font = [UIFont systemFontOfSize:42];
_baseButton.tag = 22;
[self addSubview:_baseButton];
[_baseButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(self).offset(-SIZE);
make.left.equalTo(self).offset(5 + [UIScreen mainScreen].bounds.size.width / 4 * 3);
make.width.mas_equalTo(SIZE);
make.height.mas_equalTo(SIZE);
}];
[_baseButton setBackgroundColor:[UIColor colorWithRed:0.9 green:0.58 blue:0 alpha:1]];
[_baseButton setTitle:orangeArray[4] forState:UIControlStateNormal];
[_baseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.displayTextField = [[UITextField alloc] init];
self.displayTextField.textColor = [UIColor whiteColor];
self.displayTextField.textAlignment = NSTextAlignmentRight;
self.displayTextField.font = [UIFont systemFontOfSize:88 weight:UIFontWeightLight];
self.displayTextField.text = @"0";
self.displayTextField.userInteractionEnabled = NO;
self.displayTextField.adjustsFontSizeToFitWidth = YES;
self.displayTextField.minimumFontSize = 30;
[self addSubview:self.displayTextField];
[self.displayTextField mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self).offset(140);
make.left.equalTo(self).offset(12.0);
make.right.equalTo(self).offset(-12.0);
make.height.mas_equalTo(120);
}];
}
return self;
}
@end
Model 数据处理界面
在这里就达到了整个计算器的核心,在这里的第一部分是如何把所谓的字符部分转化成我们所能处理的数据,在这里我专门写了一个函数,把字符串转化成数组,在这里我做了一个缓冲区,如果是数字就会被存放到缓冲区里面知道遇到符号的时候我们才会将数字变成一个整体加入到数组当中,这时我们的数组就是一个合格的中缀表达式,这时我们就会直接开始检测是否有不合规的括号出现或者不合规的标点符号出现,但是因为只是中缀表达是只能判读诸如8( 之类的。
下面就是我的字符串转换成数组的代码
objc
- (NSArray *)tokenize:(NSString *)expression {
NSMutableArray *tokens = [NSMutableArray array];
NSMutableString *numberBuffer = [NSMutableString string];
for (NSUInteger i = 0; i < expression.length; i++) {
unichar c = [expression characterAtIndex:i];
if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:c] || c == '.') {
[numberBuffer appendFormat:@"%C", c];
} else {
if (numberBuffer.length > 0) {
[tokens addObject:[numberBuffer copy]];
[numberBuffer setString:@""];
}
NSString *op = [NSString stringWithFormat:@"%C", c];
[tokens addObject:op];
}
}
if (numberBuffer.length > 0) {
[tokens addObject:[numberBuffer copy]];
}
for (NSUInteger i = 0; i < tokens.count; i++) {
NSString *token = tokens[i];
NSString *next = (i + 1 < tokens.count) ? tokens[i + 1] : nil;
if ([self isOperator:token] && next && [self isOperator:next]) {
return nil;
}
if ([self isNumber:token] && [next isEqualToString:@"("]) {
return nil;
}
if ([token isEqualToString:@"("] && next &&
([self isOperator:next] || [next isEqualToString:@")"])) {
return nil;
}
if ([self isOperator:token] && [next isEqualToString:@")"]) {
return nil;
}
if (i == tokens.count - 1 && [self isOperator:token]) {
return nil;
}
}
return tokens;
}
下面是中缀转后缀的部分,中缀转后缀就是我们需要做一个数组和一个栈,在这里的核心思想就是对符号顺序的一个控制,就比如如果当前词元是左括号 (,直接将它压入运算符栈 stack。如果当前词元是右括号 ),则开始一个循环,不断地从运算符栈 stack 顶部弹出元素,并将其加入到 output 输出队列,直到遇到一个左括号 ( 为止。if ([top isEqualToString:@"("]): 遇到左括号时,将它从栈中丢弃,并跳出循环。if (!foundLeftParen): 如果循环结束了还没找到左括号,说明括号不匹配,表达式非法,返回 nil。如果当前词元是运算符(+, -, ×, ÷),则执行以下逻辑:while (...): 查看运算符栈 stack 顶部的运算符,只要栈顶运算符的优先级大于或等于 当前运算符的优先级,就将栈顶运算符弹出并加入到 output 输出队列。[stack addObject:token];: while 循环结束后,将当前这个运算符压入栈中。
下面是相关代码
objc
- (NSArray *)infixToRPN:(NSArray *)tokens {
NSMutableArray *output = [NSMutableArray array];
NSMutableArray *stack = [NSMutableArray array];
NSDictionary *precedence = @{
@"+": @1, @"-": @1,
@"×": @2, @"÷": @2
};
for (NSString *token in tokens) {
if ([self isNumber:token]) {
[output addObject:token];
} else if ([token isEqualToString:@"("]) {
[stack addObject:token];
} else if ([token isEqualToString:@")"]) {
BOOL foundLeftParen = NO;
while (stack.count > 0) {
NSString *top = [stack lastObject];
[stack removeLastObject];
if ([top isEqualToString:@"("]) {
foundLeftParen = YES;
break;
} else {
[output addObject:top];
}
}
if (!foundLeftParen) {
return nil;
}
} else {
while (stack.count > 0) {
NSString *top = [stack lastObject];
if ([precedence objectForKey:top] &&
[precedence[token] integerValue] <= [precedence[top] integerValue]) {
[output addObject:top];
[stack removeLastObject];
} else {
break;
}
}
[stack addObject:token];
}
}
for (NSString *token in stack) {
if ([token isEqualToString:@"("] || [token isEqualToString:@")"]) {
return nil;
}
}
while (stack.count > 0) {
NSString *top = [stack lastObject];
[stack removeLastObject];
[output addObject:top];
}
if (tokens.count == 0) {
return nil;
}
return output;
}
最后就是逆波兰表达式的内容了,逆波兰表达式就是使用栈这一个数据结构来去通过优先级来还原我们的计算顺序。
objc
- (NSDecimalNumber *)evalRPN:(NSArray *)tokens {
NSMutableArray *stack = [NSMutableArray array];
for (NSString *token in tokens) {
if ([self isNumber:token]) {
[stack addObject:[NSDecimalNumber decimalNumberWithString:token]];
} else {
if (stack.count < 2) return nil;
NSDecimalNumber *num2 = [stack lastObject];
[stack removeLastObject];
NSDecimalNumber *num1 = [stack lastObject];
[stack removeLastObject];
NSDecimalNumber *result = nil;
if ([token isEqualToString:@"+"]) {
result = [num1 decimalNumberByAdding:num2];
} else if ([token isEqualToString:@"-"]) {
result = [num1 decimalNumberBySubtracting:num2];
} else if ([token isEqualToString:@"×"]) {
result = [num1 decimalNumberByMultiplyingBy:num2];
} else if ([token isEqualToString:@"÷"]) {
if ([num2 isEqualToNumber:@0]) return nil;
result = [num1 decimalNumberByDividingBy:num2];
}
[stack addObject:result];
}
}
return stack.count == 1 ? [stack lastObject] : nil;
}
Controller 界面
这个界面没有什么可以多说的就是单纯的绑定按钮的界面,然后在就是一些按钮的使用就比如 AC 的清零作用还有就是返回 nil 的报错提示。
下面是部分代码
objc
- (void) setupButtonEvents {
for (UIView *subview in self.caculateView.subviews) {
if ([subview isKindOfClass:[UIButton class]]) {
UIButton *btn = (UIButton *)subview;
[btn addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
}
}
}
- (void)buttonPressed:(UIButton *)sender {
NSString *title = sender.currentTitle;
if ([title isEqualToString:@"AC"]) {
[self.inputString setString:@""];
self.caculateView.displayTextField.text = @"0";
} else if ([title isEqualToString:@"="]) {
NSString *expression = [self.inputString copy];
NSDecimalNumber *result = [self.model calculateExpression:expression];
if (result) {
self.caculateView.displayTextField.text = result.stringValue;
[self.inputString setString:result.stringValue];
} else {
self.caculateView.displayTextField.text = @"错误";
[self.inputString setString:@""];
}
} else {
if (self.inputString.length >= 20) {
return;
}
[self.inputString appendString:title];
self.caculateView.displayTextField.text = self.inputString;
}
}
总结
这个项目的难点就是很多的小细节问题,需要多打磨打磨,还有就是字符串的处理问题。