计算器仿写总结
前言
计算器的主要难点在于表达式校验**、**中缀转后缀(调度场算法) 和 UI 动态适配 的实现细节。项目采用 MVC 架构,最终效果:支持括号嵌套、连续运算、负数输入、表达式实时预览,并能在数字过长时自动缩小字体并水平滚动
UI布局
HomeView 作为主界面控制器,使用Masonry自动布局库,分为三个区域
顶部表达式预览区:显示当前输入的完整算式,支持水平滚动
底部按钮网格:4 列 × 5 行的标准计算器键盘,包含数字、运算符、括号、功能键
底部按钮我们选择循环创建UIButton,循环完之后单独处理一些按钮,比如变背景色和字号色,每个按钮绑定什么方法
对于底部按键需要注意的就是按钮应该有按动的反馈

objc
- (void)buttonTouchDown:(UIButton *)sender {
[UIView animateWithDuration:0.1 animations:^{
sender.transform = CGAffineTransformMakeScale(0.92, 0.92);
sender.alpha = 0.75;
}];
}
- (void)buttonTouchUp:(UIButton *)sender {
[UIView animateWithDuration:0.1 animations:^{
sender.transform = CGAffineTransformIdentity;
sender.alpha = 1.0;
}];
}
按下时按钮会缩小到0.92,并且透明度会改变,松开按钮时恢复原样
顶部表达式应该有字体逐渐变小和可以滚动的功能
objc
- (void)updateScroll:(UIScrollView *)scroll label:(UILabel *)label text:(NSString *)text {
label.text = text;
CGFloat fontSize = 70;
if (text.length > 8) fontSize = 55;
if (text.length > 12) fontSize = 45;
if (text.length > 16) fontSize = 35;
label.font = [UIFont systemFontOfSize:fontSize];
NSDictionary *attrs = @{NSFontAttributeName : label.font};
CGFloat textWidth = [text sizeWithAttributes:attrs].width;
CGFloat labelWidth = MAX(textWidth, scroll.bounds.size.width - 40);
label.frame = CGRectMake(20, 0, labelWidth, scroll.bounds.size.height);
scroll.contentSize = CGSizeMake(labelWidth + 40, scroll.bounds.size.height);
[scroll setContentOffset:CGPointMake(scroll.contentSize.width - scroll.bounds.size.width, 0) animated:NO];
}
这段代码会随着字数的增加变三次字号,这里主要是为了美观
在这里特意强调一下,我们使用scrollerView加上UIlabel,前者可以用Masonry约束,但是后者得使用frame约束,不然不会到自己想到的地方

这个方法的下半段就是计算字符串宽度,因为每个字符的宽度是不一样的,但你的UIlabel不能简单的n * width来计算宽度,之后用结果决定label的宽度,也决定scroll的宽度,实现可以来回滚动
计算
这里主要采用中缀表达式转后缀表达式来进行计算具体可以看这篇文章《数据结构》:中缀表达式转后缀表达式 + 后缀表达式的计算
转了后缀表达式计算之后还要进行一个表达式校验,拦截非法输入,看起来更像是正常的计算器

objc
if (self.isCalculated) {
if (!inputIsOperator) {
[self.express setString:@""];
}
self.isCalculated = NO;
}
比如说按下等号后如果用户继续按数字,清空旧结果,重新开始计算,如果用户按的是运算符,则保留结果
objc
for (NSInteger i = self.express.length - 1; i >= 0; i--) {
unichar ch = [self.express characterAtIndex:i];
if (ch == '+' || ch == '-' || ch == 0x00D7 || ch == 0x00F7) {
break;
}
if (ch == '.') {
return;
}
}
小数点不能跟在运算符号后面,也不能一个数字里塞两个
我们还要考虑有括号的情况,毕竟括号不是运算符号却决定着运算的优先级,比如左括号一定要有对应的有括号,数字前后不能直接跟括号等
还有一个比较重要的地方就是减号和负号的区别了,我们采用这样的逻辑,如果最后一个字符是 -,且它前面是 + - × ÷ (,或者它本身就是第一个字符,那么这个 - 就是负号
objc
if (ch == '-') {
if (i == 0) {
isNegativeSign = YES;
} else {
unichar prev = [self.express characterAtIndex:i - 1];
if (prev == '+' || prev == '-' || prev == 0x00D7 || prev == 0x00F7 || prev == '(') {
isNegativeSign = YES;
}
}
}
if ((ch >= '0' && ch <= '9') || ch == '.' || isNegativeSign) {
[number appendFormat:@"%C", ch];
} else {
if (number.length > 0) {
[self.infixArray addObject:[number copy]];
[number setString:@""];
}
NSString *op = [NSString stringWithFormat:@"%C", ch];
[self.infixArray addObject:op];
}
}
其他需要注意的方面有数学上就非法的输入,比如除以0的情况,系统默认会给你一个inf,不过有个问题就是inf后如果进行运算,inf并不会参与,所以我们干脆直接让结果为error,开始下一次运算,还有其他不合法的输入都可以让结果为error
总结
这个项目的ui难度并不是很难,但是多种多样的情况需要考虑并去处理