Calculator - 计算器

1 + 2 + 3 = 6

Calculator - 计算器 - 图1

这是一个计算器应用程序,你可以在这里下载这个例子


简介

这里的计算器是用响应式编程写的,而且它还用到了 RxFeedback 架构。它比较适合有经验的 RxSwift 使用者学习。接下来我们就来介绍一下这个应用程序是如何实现的。


整体结构

Calculator - 计算器 - 图2

  1. class CalculatorViewController: ViewController {
  2. @IBOutlet weak var lastSignLabel: UILabel!
  3. @IBOutlet weak var resultLabel: UILabel!
  4. @IBOutlet weak var allClearButton: UIButton!
  5. @IBOutlet weak var changeSignButton: UIButton!
  6. @IBOutlet weak var percentButton: UIButton!
  7. @IBOutlet weak var divideButton: UIButton!
  8. @IBOutlet weak var multiplyButton: UIButton!
  9. @IBOutlet weak var minusButton: UIButton!
  10. @IBOutlet weak var plusButton: UIButton!
  11. @IBOutlet weak var equalButton: UIButton!
  12. @IBOutlet weak var dotButton: UIButton!
  13. @IBOutlet weak var zeroButton: UIButton!
  14. @IBOutlet weak var oneButton: UIButton!
  15. @IBOutlet weak var twoButton: UIButton!
  16. @IBOutlet weak var threeButton: UIButton!
  17. @IBOutlet weak var fourButton: UIButton!
  18. @IBOutlet weak var fiveButton: UIButton!
  19. @IBOutlet weak var sixButton: UIButton!
  20. @IBOutlet weak var sevenButton: UIButton!
  21. @IBOutlet weak var eightButton: UIButton!
  22. @IBOutlet weak var nineButton: UIButton!
  23. override func viewDidLoad() {
  24. let commands: Observable<CalculatorCommand> = Observable.merge([
  25. allClearButton.rx.tap.map { _ in .clear },
  26. changeSignButton.rx.tap.map { _ in .changeSign },
  27. percentButton.rx.tap.map { _ in .percent },
  28. divideButton.rx.tap.map { _ in .operation(.division) },
  29. multiplyButton.rx.tap.map { _ in .operation(.multiplication) },
  30. minusButton.rx.tap.map { _ in .operation(.subtraction) },
  31. plusButton.rx.tap.map { _ in .operation(.addition) },
  32. equalButton.rx.tap.map { _ in .equal },
  33. dotButton.rx.tap.map { _ in .addDot },
  34. zeroButton.rx.tap.map { _ in .addNumber("0") },
  35. oneButton.rx.tap.map { _ in .addNumber("1") },
  36. twoButton.rx.tap.map { _ in .addNumber("2") },
  37. threeButton.rx.tap.map { _ in .addNumber("3") },
  38. fourButton.rx.tap.map { _ in .addNumber("4") },
  39. fiveButton.rx.tap.map { _ in .addNumber("5") },
  40. sixButton.rx.tap.map { _ in .addNumber("6") },
  41. sevenButton.rx.tap.map { _ in .addNumber("7") },
  42. eightButton.rx.tap.map { _ in .addNumber("8") },
  43. nineButton.rx.tap.map { _ in .addNumber("9") }
  44. ])
  45. let system = Observable.system(
  46. CalculatorState.initial,
  47. accumulator: CalculatorState.reduce,
  48. scheduler: MainScheduler.instance,
  49. feedback: { _ in commands }
  50. )
  51. .debug("calculator state")
  52. .share(replay: 1)
  53. system.map { $0.screen }
  54. .bind(to: resultLabel.rx.text)
  55. .disposed(by: disposeBag)
  56. system.map { $0.sign }
  57. .bind(to: lastSignLabel.rx.text)
  58. .disposed(by: disposeBag)
  59. }
  60. func formatResult(_ result: String) -> String {
  61. if result.hasSuffix(".0") {
  62. return result.substring(to: result.index(result.endIndex, offsetBy: -2))
  63. } else {
  64. return result
  65. }
  66. }
  67. }

首先合成出一个命令序列,它是通过按钮点击转换过来的:

Calculator - 计算器 - 图3

  1. let commands: Observable<CalculatorCommand> = Observable.merge([
  2. allClearButton.rx.tap.map { _ in .clear },
  3. changeSignButton.rx.tap.map { _ in .changeSign },
  4. percentButton.rx.tap.map { _ in .percent },
  5. divideButton.rx.tap.map { _ in .operation(.division) },
  6. multiplyButton.rx.tap.map { _ in .operation(.multiplication) },
  7. minusButton.rx.tap.map { _ in .operation(.subtraction) },
  8. plusButton.rx.tap.map { _ in .operation(.addition) },
  9. equalButton.rx.tap.map { _ in .equal },
  10. dotButton.rx.tap.map { _ in .addDot },
  11. zeroButton.rx.tap.map { _ in .addNumber("0") },
  12. oneButton.rx.tap.map { _ in .addNumber("1") },
  13. twoButton.rx.tap.map { _ in .addNumber("2") },
  14. threeButton.rx.tap.map { _ in .addNumber("3") },
  15. fourButton.rx.tap.map { _ in .addNumber("4") },
  16. fiveButton.rx.tap.map { _ in .addNumber("5") },
  17. sixButton.rx.tap.map { _ in .addNumber("6") },
  18. sevenButton.rx.tap.map { _ in .addNumber("7") },
  19. eightButton.rx.tap.map { _ in .addNumber("8") },
  20. nineButton.rx.tap.map { _ in .addNumber("9") }
  21. ])

通过使用 map 方法将按钮点击事件转换为对应的命令。如:将 allClearButton 点击事件转换为清除命令,将 plusButton 点击事件转换为相加命令,将 oneButton 点击事件转换为添加数字1命令。最后使用 merge 操作符将这些命令合并。于是就得到了我们所需要的命令序列

几乎每个页面都是有状态的。我们通过命令序列来对状态进行修改,然后产生一个新的状态。例如,刚进页面后,点击了按钮 1 。那么初始状态为 0,在执行添加数字1命令后,状态就更新为 1。通过这种变换方式,就可以生成一个状态序列

Calculator - 计算器 - 图4

  1. let system = Observable.system(
  2. CalculatorState.initial,
  3. accumulator: CalculatorState.reduce,
  4. scheduler: MainScheduler.instance,
  5. feedback: { _ in commands }
  6. )
  7. .debug("calculator state")
  8. .share(replay: 1)

命令序列触发,对页面状态进行更新,在用更新后的状态组成一个序列。这就是我们所需要的状态序列。接下来我们用这个状态序列来控制页面显示:

Calculator - 计算器 - 图5

  1. system.map { $0.screen }
  2. .bind(to: resultLabel.rx.text)
  3. .disposed(by: disposeBag)
  4. system.map { $0.sign }
  5. .bind(to: lastSignLabel.rx.text)
  6. .disposed(by: disposeBag)

state.screen 来控制 resultLabel 的显示内容。用 state.sign 来控制 lastSignLabel 的显示内容。


Calculator

控制器主要负责数据绑定,而整个计算器的大脑Calculator.swift 文件内。

State:

这个页面主要有三种状态:

  1. enum CalculatorState {
  2. case oneOperand(screen: String)
  3. case oneOperandAndOperator(operand: Double, operator: Operator)
  4. case twoOperandsAndOperator(operand: Double, operator: Operator, screen: String)
  5. }
  • oneOperand 一个操作数,例如:进入页面后,输入 1 时的状态
  • oneOperandAndOperator 一个操作数和一个运算符,例如:进入页面后,输入 1 + 时的状态
  • twoOperandsAndOperator 两个操作数和一个运算符,例如:进入页面后,输入 1 + 2 时的状态

Command:

这个计算器提供七种命令:

  1. enum Operator {
  2. case addition
  3. case subtraction
  4. case multiplication
  5. case division
  6. }
  7. enum CalculatorCommand {
  8. case clear
  9. case changeSign
  10. case percent
  11. case operation(Operator)
  12. case equal
  13. case addNumber(Character)
  14. case addDot
  15. }
  • clear 清除,重置
  • changeSign 改变正负号
  • percent 百分比
  • operation 四则运算
  • equal 等于
  • addNumber 输入数字
  • addDot 输入 “.”

reduce:

当命令产生时,将它应用到当前状态上,然后生成新的状态:

Calculator - 计算器 - 图6

  1. extension CalculatorState {
  2. static func reduce(state: CalculatorState, _ x: CalculatorCommand) -> CalculatorState {
  3. switch x {
  4. case .clear:
  5. return CalculatorState.initial
  6. case .addNumber(let c):
  7. return state.mapScreen { $0 == "0" ? String(c) : $0 + String(c) }
  8. case .addDot:
  9. return state.mapScreen { $0.range(of: ".") == nil ? $0 + "." : $0 }
  10. case .changeSign:
  11. return state.mapScreen { "\(-(Double($0) ?? 0.0))" }
  12. case .percent:
  13. return state.mapScreen { "\((Double($0) ?? 0.0) / 100.0)" }
  14. case .operation(let o):
  15. switch state {
  16. case let .oneOperand(screen):
  17. return .oneOperandAndOperator(operand: screen.doubleValue, operator: o)
  18. case let .oneOperandAndOperator(operand, _):
  19. return .oneOperandAndOperator(operand: operand, operator: o)
  20. case let .twoOperandsAndOperator(operand, oldOperator, screen):
  21. return .twoOperandsAndOperator(operand: oldOperator.perform(operand, screen.doubleValue), operator: o, screen: "0")
  22. }
  23. case .equal:
  24. switch state {
  25. case let .twoOperandsAndOperator(operand, operat, screen):
  26. let result = operat.perform(operand, screen.doubleValue)
  27. return .oneOperand(screen: String(result))
  28. default:
  29. return state
  30. }
  31. }
  32. }
  33. }
  • clear 重置当前状态
  • addNumber, addDot, changeSign, percent 只需要更改屏显即可
  • operation 需要根据当前状态来确定如何变化状态。
    • 如果只有一个操作数,就添加操作符。
    • 如果有一个操作数和操作符,就替换操作符。
    • 如果有两个操作数和一个操作符,将他们的计算结果作为操作数保留,然后加入新的操作符,以及一个操作数 0.
  • equal 如果当前有两个操作数和一个操作符,将他们的计算结果作为操作数保留。否则什么都不做。

剩下的都是一些辅助代码,接下来我们再来看下全部代码:

ViewController:

  1. class CalculatorViewController: ViewController {
  2. @IBOutlet weak var lastSignLabel: UILabel!
  3. @IBOutlet weak var resultLabel: UILabel!
  4. @IBOutlet weak var allClearButton: UIButton!
  5. @IBOutlet weak var changeSignButton: UIButton!
  6. @IBOutlet weak var percentButton: UIButton!
  7. @IBOutlet weak var divideButton: UIButton!
  8. @IBOutlet weak var multiplyButton: UIButton!
  9. @IBOutlet weak var minusButton: UIButton!
  10. @IBOutlet weak var plusButton: UIButton!
  11. @IBOutlet weak var equalButton: UIButton!
  12. @IBOutlet weak var dotButton: UIButton!
  13. @IBOutlet weak var zeroButton: UIButton!
  14. @IBOutlet weak var oneButton: UIButton!
  15. @IBOutlet weak var twoButton: UIButton!
  16. @IBOutlet weak var threeButton: UIButton!
  17. @IBOutlet weak var fourButton: UIButton!
  18. @IBOutlet weak var fiveButton: UIButton!
  19. @IBOutlet weak var sixButton: UIButton!
  20. @IBOutlet weak var sevenButton: UIButton!
  21. @IBOutlet weak var eightButton: UIButton!
  22. @IBOutlet weak var nineButton: UIButton!
  23. override func viewDidLoad() {
  24. let commands: Observable<CalculatorCommand> = Observable.merge([
  25. allClearButton.rx.tap.map { _ in .clear },
  26. changeSignButton.rx.tap.map { _ in .changeSign },
  27. percentButton.rx.tap.map { _ in .percent },
  28. divideButton.rx.tap.map { _ in .operation(.division) },
  29. multiplyButton.rx.tap.map { _ in .operation(.multiplication) },
  30. minusButton.rx.tap.map { _ in .operation(.subtraction) },
  31. plusButton.rx.tap.map { _ in .operation(.addition) },
  32. equalButton.rx.tap.map { _ in .equal },
  33. dotButton.rx.tap.map { _ in .addDot },
  34. zeroButton.rx.tap.map { _ in .addNumber("0") },
  35. oneButton.rx.tap.map { _ in .addNumber("1") },
  36. twoButton.rx.tap.map { _ in .addNumber("2") },
  37. threeButton.rx.tap.map { _ in .addNumber("3") },
  38. fourButton.rx.tap.map { _ in .addNumber("4") },
  39. fiveButton.rx.tap.map { _ in .addNumber("5") },
  40. sixButton.rx.tap.map { _ in .addNumber("6") },
  41. sevenButton.rx.tap.map { _ in .addNumber("7") },
  42. eightButton.rx.tap.map { _ in .addNumber("8") },
  43. nineButton.rx.tap.map { _ in .addNumber("9") }
  44. ])
  45. let system = Observable.system(
  46. CalculatorState.initial,
  47. accumulator: CalculatorState.reduce,
  48. scheduler: MainScheduler.instance,
  49. feedback: { _ in commands }
  50. )
  51. .debug("calculator state")
  52. .share(replay: 1)
  53. system.map { $0.screen }
  54. .bind(to: resultLabel.rx.text)
  55. .disposed(by: disposeBag)
  56. system.map { $0.sign }
  57. .bind(to: lastSignLabel.rx.text)
  58. .disposed(by: disposeBag)
  59. }
  60. func formatResult(_ result: String) -> String {
  61. if result.hasSuffix(".0") {
  62. return result.substring(to: result.index(result.endIndex, offsetBy: -2))
  63. } else {
  64. return result
  65. }
  66. }
  67. }

Calculator:

  1. enum Operator {
  2. case addition
  3. case subtraction
  4. case multiplication
  5. case division
  6. }
  7. enum CalculatorCommand {
  8. case clear
  9. case changeSign
  10. case percent
  11. case operation(Operator)
  12. case equal
  13. case addNumber(Character)
  14. case addDot
  15. }
  16. enum CalculatorState {
  17. case oneOperand(screen: String)
  18. case oneOperandAndOperator(operand: Double, operator: Operator)
  19. case twoOperandsAndOperator(operand: Double, operator: Operator, screen: String)
  20. }
  21. extension CalculatorState {
  22. static let initial = CalculatorState.oneOperand(screen: "0")
  23. func mapScreen(transform: (String) -> String) -> CalculatorState {
  24. switch self {
  25. case let .oneOperand(screen):
  26. return .oneOperand(screen: transform(screen))
  27. case let .oneOperandAndOperator(operand, operat):
  28. return .twoOperandsAndOperator(operand: operand, operator: operat, screen: transform("0"))
  29. case let .twoOperandsAndOperator(operand, operat, screen):
  30. return .twoOperandsAndOperator(operand: operand, operator: operat, screen: transform(screen))
  31. }
  32. }
  33. var screen: String {
  34. switch self {
  35. case let .oneOperand(screen):
  36. return screen
  37. case .oneOperandAndOperator:
  38. return "0"
  39. case let .twoOperandsAndOperator(_, _, screen):
  40. return screen
  41. }
  42. }
  43. var sign: String {
  44. switch self {
  45. case .oneOperand:
  46. return ""
  47. case let .oneOperandAndOperator(_, o):
  48. return o.sign
  49. case let .twoOperandsAndOperator(_, o, _):
  50. return o.sign
  51. }
  52. }
  53. }
  54. extension CalculatorState {
  55. static func reduce(state: CalculatorState, _ x: CalculatorCommand) -> CalculatorState {
  56. switch x {
  57. case .clear:
  58. return CalculatorState.initial
  59. case .addNumber(let c):
  60. return state.mapScreen { $0 == "0" ? String(c) : $0 + String(c) }
  61. case .addDot:
  62. return state.mapScreen { $0.range(of: ".") == nil ? $0 + "." : $0 }
  63. case .changeSign:
  64. return state.mapScreen { "\(-(Double($0) ?? 0.0))" }
  65. case .percent:
  66. return state.mapScreen { "\((Double($0) ?? 0.0) / 100.0)" }
  67. case .operation(let o):
  68. switch state {
  69. case let .oneOperand(screen):
  70. return .oneOperandAndOperator(operand: screen.doubleValue, operator: o)
  71. case let .oneOperandAndOperator(operand, _):
  72. return .oneOperandAndOperator(operand: operand, operator: o)
  73. case let .twoOperandsAndOperator(operand, oldOperator, screen):
  74. return .twoOperandsAndOperator(operand: oldOperator.perform(operand, screen.doubleValue), operator: o, screen: "0")
  75. }
  76. case .equal:
  77. switch state {
  78. case let .twoOperandsAndOperator(operand, operat, screen):
  79. let result = operat.perform(operand, screen.doubleValue)
  80. return .oneOperand(screen: String(result))
  81. default:
  82. return state
  83. }
  84. }
  85. }
  86. }
  87. extension Operator {
  88. var sign: String {
  89. switch self {
  90. case .addition: return "+"
  91. case .subtraction: return "-"
  92. case .multiplication: return "×"
  93. case .division: return "/"
  94. }
  95. }
  96. var perform: (Double, Double) -> Double {
  97. switch self {
  98. case .addition: return (+)
  99. case .subtraction: return (-)
  100. case .multiplication: return (*)
  101. case .division: return (/)
  102. }
  103. }
  104. }
  105. private extension String {
  106. var doubleValue: Double {
  107. guard let double = Double(self) else {
  108. return Double.infinity
  109. }
  110. return double
  111. }
  112. }

参考