使用 SwfitUI 来做仿做一个原生计算器
概述
这里我们将会使用MVVM架构来编写这个项目
代码已开源 { :.prompt-info}
Views
calcButtonView
:单个按钮样式
contentsView
:计算器app样式
ViewModel(ContentViewModel
)
变量定义
1
2
3
4
5
6
7
8
9
10
11
12
13
@Published var display: String = "0" //计算器显示字符
@Published var action: Action? //按键操作
@Published var constantlyLit: Bool = false //操作是否常亮
@Published var ACPressed = false //是否按下AC键
private var operandStack: [String] = [] //数字栈
private var operatorStack: [String] = [] //符号栈
private var lastPressedButton: lastOprationType = .clear//上次按下按钮类型
private var shouldClearAll: Bool = true //清除全部栈还是清除当前数字栈的最后一位
private var lastNum = "" //上次按下数字
private var lastOperator = "" //上次按下操作
var canDeleteLastDigit = true //是否允许左右滑动显示界面来删除最后一位
var stacked = false //是否有元素入栈
基本运算逻辑
按键类型
- 数字(0~9)
- 运算(\(+\),\(-\),\(\times\),\(\div\) )
- 符号切换键(\(+/-\))
- 百分号(\(\%\))
- 等于(\(=\))
- 清除(AC)
数字处理逻辑
追加:当上一个输入的是数字时,追加新数字。如果数字栈中最后一个元素是0,则直接显示当前数字。数字除去小数点后最大长度为9。
清除重新显示:当上次进行了运算,显示当前数字;若上次进行了特殊操作或清除,先清空栈再显示当前数字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if lastPressedButton == .operand {
var operand = operandStack.popLast() ?? "0"
if operand == "0" {
operand = text
} else {
operand.append(text)
}
operandStack.append(operand)
lastPressedButton = .operand
} else if lastPressedButton == .clear || lastPressedButton == .specialOperator {
_ = operandStack.popLast()
operandStack.append(text)
}
else {
display = text
operandStack.append(text)
}
lastPressedButton = .operand
运算处理逻辑
- 更改算符:上一个输入也为算符,并且当前操作不是 \(=\) 或 \(+/-\) 则替换符号栈最后一个符号
1
2
3
if lastPressedButton == .operation && action != .equals && action != .plusMinus{
_ = operatorStack.popLast()
}
- 压入运算符栈:上一个运算符优先级小于等于当前运算符优先级
- \(+ \space - \space \times \space \div\)进行运算:上一个运算符优先级大于当前运算符优先级
当输入运算符后,运算符栈内只可能有3种情况:
- 有一个运算符:运算符为刚输入的运算符,则不做任何操作
- 有两个运算符:可能组合为低优先级+高优先级,或高优先级+低优先级,后者对高优先级进行处理
- 有三个运算符:可能组合为低优先级+高优先级+高优先级,例如\(+ \space \times \space \times\),或低优先级+高优先级+低优先级,例如\(+ \space \times \space +\)
符号切换键处理逻辑
- 当上个操作为运算符时,将“-0”压入数字栈
- 当上个操作不为运算符时,取出上个数字,并取相反数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if lastPressedButton == .operation {
operandStack.append("-0")
display = "-0"
} else {
var last = operandStack.popLast() ?? "0"
if last.first == "-" {
last.removeFirst()
} else {
last = "-" + last
}
operandStack.append(last)
display = last
}
lastPressedButton = .plusMinus
百分号处理逻辑
- 数字栈中最后一个元素 \(\div\) 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
canDeleteLastDigit = false
if operandStack.last == "0" && operandStack.last == nil {
lastPressedButton = .percent
return
} else if lastPressedButton == .operand || lastPressedButton == .percent || lastPressedButton == .equal {
let numString = operandStack.popLast() ?? "0"
let num = (convertToDouble(numString) ?? 0.0) / 100.0
operandStack.append(String(num))
display = convertToString(num)
} else if lastPressedButton == .operation {
let numString = operandStack.popLast() ?? "0"
let num = (convertToDouble(numString) ?? 0.0) / 100.0
operandStack.append(numString)
operandStack.append(String(num))
display = convertToString(num)
}
lastPressedButton = .percent
等于号处理逻辑
将数字栈与符号栈进行组合运算
按下等于号前,符号栈内可能情况为
- 无运算符:则重复运算上次运算符与操作数组合。5-2*3 -> -1 -3 -9 -27
- 有一个运算符:
- 有两个运算符:组合为低优先级+高优先级
- 有三个运算符
1
2
3
4
5
6
7
8
9
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
49
private func equalButtonPressed() {
canDeleteLastDigit = false
if lastPressedButton == .operation,
let operand = operandStack.last
{
operandStack.append(operand)
}
if operatorStack.count == 1{
guard
let operation = operatorStack.popLast(),
let rightNum = operandStack.popLast(),
let leftNum = operandStack.popLast()
else {return}
lastOperator = operation
lastNum = rightNum
let answer = solve(leftNum: leftNum, operation: operation, rightNum: rightNum)
operandStack.append(answer)
display = answer
} else if operatorStack.count == 2{
guard
let secondOperator = operatorStack.popLast(),
let firstOperator = operatorStack.popLast(),
let thirdNum = operandStack.popLast(),
let secondNum = operandStack.popLast(),
let firstNum = operandStack.popLast()
else { return }
lastOperator = secondOperator
lastNum = thirdNum
let answer1 = solve(leftNum: secondNum, operation: secondOperator, rightNum: thirdNum)
let answer2 = solve(leftNum: firstNum, operation: firstOperator, rightNum: answer1)
operandStack.append(answer2)
display = answer2
} else {
// count == 0
if lastNum == "" && lastOperator == ""
{
display = operandStack.last ?? "0"
} else {
let operand = operandStack.popLast() ?? "0"
let answer = solve(leftNum: operand, operation: lastOperator, rightNum: lastNum)
operandStack.append(answer)
display = answer
}
}
}
清除处理逻辑
恢复初始设置
1
2
3
4
5
6
7
8
9
10
11
12
private func clearAll() {
display = "0"
action = nil
constantlyLit = false
canDeleteLastDigit = false
ACPressed = true
operandStack = []
operatorStack = []
stacked = false
lastNum = ""
lastOperator = ""
}
显示处理逻辑
- 数字栈为空则显示0
- 数字栈不为空则显示栈内最后一个元素
- 显示最大长度为9个字符,长于9个使用科学计数法,2.81531e10,3.362826e9,除去小数点和负号,最长显示9个字符
- 对超过显示范围的数字使用科学计数法(代码中限制了精度为\(10^{-15}\))
2.457343726 -> 2.45734373
2.81531e10
3.72529e-9
测试集
\[3 + 2 \times 5 - 4\] \[3 \times 2 + 5 \times 4\] \[3 + 2 = \% \% \% \% \%\] \[1 \div 4 = = = = = = = = = = = = = = =\] \[123 \times = = = = =\] \[5 + \pm 3 =\] \[5+ \pm =\]附加功能
表情雨
This post is licensed under CC BY 4.0 by the author.