[iOS] 计算器仿写

[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;
    }
}

总结

这个项目的难点就是很多的小细节问题,需要多打磨打磨,还有就是字符串的处理问题。

相关推荐
2501_929157685 小时前
【安卓+PC+IOS】psp全中文游戏+高清纹理包+金手指
android·游戏·ios
2501_916008896 小时前
iOS 混淆工具链实战 多工具组合完成 IPA 混淆与加固(iOS混淆|IPA加固|无源码加固|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview
自在极意功。10 小时前
贪心算法深度解析:从理论到实战的完整指南
java·算法·ios·贪心算法
游戏开发爱好者815 小时前
HTTPS 内容抓取实战 能抓到什么、怎么抓、不可解密时如何定位(面向开发与 iOS 真机排查)
android·网络协议·ios·小程序·https·uni-app·iphone
齐行超1 天前
SwiftUI NavigatorStack 导航容器
ios·swiftui·navigationstack·navigationpath·navigationlink
QuantumLeap丶1 天前
《Flutter全栈开发实战指南:从零到高级》- 05 - 基础组件实战:构建登录界面
flutter·ios
黄毛火烧雪下1 天前
(四)Flutter插件之IOS插件开发
flutter·ios
2501_915106321 天前
iOS 混淆与 IPA 加固全流程,多工具组合实现无源码混淆、源码防护与可审计流水线(iOS 混淆|IPA 加固|无源码加固|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview
游戏开发爱好者81 天前
用多工具组合把 iOS 混淆做成可复用的工程能力(iOS混淆 IPA加固 无源码混淆 Ipa Guard)
android·ios·小程序·https·uni-app·iphone·webview