VCG41N1227181130.jpg
上一篇我们写出了计算器的ui,这一章我们开始实现功能.

计算类具体实现

新建类 CalculatorBrain.swift 里面来实现具体的计算:

  1. //
  2. // CalculatorBrain.swift
  3. // Calculator
  4. //
  5. // Created by wei on 2021/7/5.
  6. //
  7. import Foundation
  8. /// 计算器 app 的逻辑部分
  9. enum CalculatorBrain {
  10. ///用户正在输入左侧数字ing . 当用户按下加减乘除改变为下一个状态
  11. case left(String)
  12. ///用户输入了左侧数字和计算符号,等待开始输入右侧数字
  13. case leftOp(left: String,op: CalculatorButtonItem.Op)
  14. ///用户已经输入了左侧数字、计算符号、和部分右侧数字.并在等待更多右侧数字的输入
  15. case leftOpRight(left: String,op: CalculatorButtonItem.Op,right: String)
  16. ///输入计算出现了错误.
  17. case error
  18. /// 输出 字符串的显示结果
  19. var output: String{
  20. let result : String
  21. switch self {
  22. //如果输入的左侧的数字,直接输出left的数字
  23. case .left(let left): result = left
  24. case .leftOp(let leftOp, _): result = leftOp
  25. case .leftOpRight(let leftOpRight, _, _): result = leftOpRight
  26. case .error: result = "Error"
  27. }
  28. guard let value = Double(result) else {
  29. return "Error"
  30. }
  31. //限制结果为8位小数后输出
  32. return formatter.string(from: value as NSNumber)!
  33. }
  34. /// 把item点击事件转变成brain输出
  35. /// - Parameter item: <#item description#>
  36. /// - Returns: <#description#>
  37. @discardableResult
  38. func apply(item: CalculatorButtonItem) -> CalculatorBrain {
  39. switch item {
  40. case .digit(let num):
  41. return apply(num: num)
  42. case .dot:
  43. return applyDot()
  44. case .op(let op):
  45. return apply(op: op)
  46. case .command(let command):
  47. return apply(command: command)
  48. }
  49. }
  50. /// 把输入的数值(0-9)变成brain
  51. /// - Parameter num: 0-9的值
  52. /// - Returns: brain对象
  53. private func apply(num: Int) -> CalculatorBrain {
  54. switch self {
  55. case .left(let left):
  56. return .left(left.apply(num: num))
  57. case .leftOp(let left,let op):
  58. return .leftOpRight(left: left, op: op, right: "0".apply(num: num))
  59. case .leftOpRight(let left, let op, let right):
  60. return .leftOpRight(left: left, op: op, right: right.apply(num: num))
  61. case .error:
  62. return .left("0".apply(num: num))
  63. }
  64. }
  65. /// 把小数点变成brain对象
  66. /// - Returns: brain对象
  67. private func applyDot() -> CalculatorBrain {
  68. switch self {
  69. case .left(let left):
  70. return .left(left.applyDot())
  71. case .leftOp(let left, let op):
  72. return .leftOpRight(left: left, op: op, right: "0".applyDot())
  73. case .leftOpRight(let left, let op, let right):
  74. return .leftOpRight(left: left, op: op, right: right.applyDot())
  75. case .error:
  76. return .left("0".applyDot())
  77. }
  78. }
  79. /// 把运算符转变成brain对象
  80. /// - Parameter op: 运算符号
  81. /// - Returns: brain对象
  82. private func apply(op: CalculatorButtonItem.Op) -> CalculatorBrain {
  83. switch self {
  84. case .left(let left):
  85. switch op{
  86. case .plus, .minus, .divide, .multipy:
  87. return .leftOp(left: left, op: op)
  88. case .equal:
  89. return self
  90. }
  91. case .leftOp(let left, let currentop):
  92. switch op {
  93. case .plus, .minus, .divide, .multipy:
  94. return .leftOp(left: left, op: currentop)
  95. case .equal:
  96. if let result = currentop.calculate(l: left, r: left) {
  97. return .leftOp(left: result, op: currentop)
  98. }else{
  99. return .error
  100. }
  101. }
  102. case .leftOpRight(let left, let currentop, let right):
  103. switch op {
  104. case .plus, .minus, .divide, .multipy:
  105. if let result = currentop.calculate(l: left, r: right) {
  106. return .leftOp(left: result, op: currentop)
  107. }else{
  108. return .error
  109. }
  110. case .equal:
  111. if let result = currentop.calculate(l: left, r: right){
  112. return .left(result)
  113. }else{
  114. return .error
  115. }
  116. }
  117. case .error:
  118. return self
  119. }
  120. }
  121. /// 把其他的符号转变成brain对象
  122. /// - Parameter command: 情况、反正、取余
  123. /// - Returns: brain对象
  124. private func apply(command: CalculatorButtonItem.Command) -> CalculatorBrain {
  125. switch command {
  126. case .clear:
  127. return .left("0")
  128. case .flip:
  129. switch self {
  130. case .left(let left):
  131. return .left(left.flipped())
  132. case .leftOp(let left, let op):
  133. return .leftOpRight(left: left, op: op, right: "-0")
  134. case .leftOpRight(let left, let op, let right):
  135. return .leftOpRight(left: left, op: op, right: right)
  136. case .error:
  137. return .left("-0")
  138. }
  139. case .percent:
  140. switch self {
  141. case .left(let left):
  142. return .left(left.percentaged())
  143. case .leftOp:
  144. return self
  145. case .leftOpRight(left: let left, let op, let right):
  146. return .leftOpRight(left: left, op: op, right: right.percentaged())
  147. case .error:
  148. return .left("-0")
  149. }
  150. }
  151. }
  152. }
  153. ///将结果的数字小数后位数,限定在八位以内
  154. var formatter: NumberFormatter = {
  155. let f = NumberFormatter()
  156. f.minimumFractionDigits = 0
  157. f.maximumFractionDigits = 8
  158. f.numberStyle = .decimal
  159. return f
  160. }()
  161. extension String {
  162. ///是否包含小数点
  163. var containsDot: Bool {
  164. return contains(".")
  165. }
  166. ///开始减去
  167. var startWithNegative: Bool {
  168. return starts(with: "-")
  169. }
  170. //int as str
  171. func apply(num: Int) -> String {
  172. return self == "0" ? "\(num)" : "\(self)\(num)"
  173. }
  174. func applyDot() -> String {
  175. return containsDot ? self : "\(self)."
  176. }
  177. ///反转-号
  178. func flipped() -> String {
  179. if startWithNegative {
  180. var s = self
  181. s.removeFirst()
  182. return s
  183. } else {
  184. return "-\(self)"
  185. }
  186. }
  187. ///求百分比
  188. func percentaged() -> String {
  189. return String(Double(self)! / 100)
  190. }
  191. }
  192. extension CalculatorButtonItem.Op {
  193. ///点击=号的时候计算
  194. func calculate(l: String, r: String) -> String? {
  195. ///左右值都不为空继续,为空返回nil
  196. guard let left = Double(l), let right = Double(r) else {
  197. return nil
  198. }
  199. ///结果
  200. let result: Double?
  201. switch self {
  202. case .plus: result = left + right
  203. case .minus: result = left - right
  204. case .multipy: result = left * right
  205. case .divide: result = right == 0 ? nil : left / right
  206. case .equal: fatalError()
  207. }
  208. print("= result \(String(describing: result))")
  209. return result.map { String($0) }
  210. }
  211. }

使用 @state 关联UI

在主工程里面,初始化brain对象,然后加上修饰@state,初始化值为0

@state 修饰的值,在SwiftUI 内部会被自动转换为一对setter 和getter,对这个属性进行赋值的操作将会触发View 的刷新,它的body 会被再次调用,底层渲染引擎会找出界面上被改变的部分,根据新的属性值计算出新 的View,并进行刷新。 @State 属性值仅只能在属性本身被设置时会触发UI 刷新,这个特性让它非常适合用来声明一个值类型的值

对于@State 修饰的属性的访问,只能发生在body 或者body 所调用的方法中。你不能在外部改变@State 的值,它的所有相关操作和状态改变都应该是和当前View 挂钩的

///主内容视图
struct ContentView: View {

    //state 在swiftui中会自动转换为一对 setter 和 getter. 对这个属性进行赋值
    //的操作将会触发 view的刷新
    @State private var brain: CalculatorBrain = .left("0")

    var body: some View {
        //垂直布局
        VStack(spacing: 12, content: {
            Spacer() //为了填充上面的留白
            Text(brain.output) //输入显示的区域
                .font(.system(size: 76))
                .minimumScaleFactor(0.5)
                .padding(.trailing, 24 * scale)
                .frame(
                    minWidth: 0,
                    maxWidth: .infinity,  //最大
                    alignment: .trailing  //靠右
                )
            Button("Test") {
                self.brain = .left("1.23")
            }
            CalculatorButtonPad(brain: $brain)  //计算器上面的按键
                .padding(.bottom)  //底部留白为0
        })
    }
}

使用@state修饰的值,在对象之间传递的之后会遵循值语义发生复制,这样一层层传递下去的话,text是无法跟新的,怎么才能保持一样呢?我们用的@Binding

使用@Binding保持引用语义

@Binding 也是对属性的修饰,它做的事情是将值语义的属性“转换” 为引用语义。对被声明为@Binding 的属
性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递。

我们在 CalculatorButtonPad 和CalculatorButtonRow 的定义,分别为它们加上 @Binding 的brain:

/// 封装的每一行 ,使用foreach
struct CalculatorButtonRow: View {

    @Binding var brain: CalculatorBrain

    let row: [CalculatorButtonItem] //每一行里面的元素button

    var body: some View {
        HStack{  //水平布局
            ForEach(row, id: \.self) { item in  //使用foreach水平布局button
                CalculatorButton(
                    title: item.title,
                    size: item.size,
                    backgroundColorName: item.backgroundColorName) {
                    print("button : \(item.title)")
                    self.brain = self.brain.apply(item: item)
                }
            }
        }
    }
}

/// 封装计算器上的所有按键
struct CalculatorButtonPad: View {

    @Binding var brain: CalculatorBrain

    /// pad里面是每一行的数据,具体意思看下item类
    let pad: [[CalculatorButtonItem]] = [
        [.command(.clear), .command(.flip), .command(.percent), .op(.divide)],
        [.digit(7), .digit(8), .digit(9), .op(.multipy)],
        [.digit(4), .digit(5), .digit(6), .op(.minus)],
        [.digit(1), .digit(2), .digit(3), .op(.plus)],
        [.digit(0), .dot, .op(.equal)],
    ]

    var body: some View {
        //垂直布局类
        VStack(spacing: 8, content: {
            //使用foreach添加每一行
            ForEach(pad, id: \.self) { row in
                CalculatorButtonRow(brain: self.$brain, row: row)//每一行的数据
            }
        })
    }
}

在初始化他的地方修改为:

 CalculatorButtonPad(brain: $brain)  //计算器上面的按键
 CalculatorButtonRow(brain: self.$brain, row: row)//每一行的数据

在用@Binding修饰的属性前面我们加上美元$符号.

对一个由@ 符号修饰的属性,在它前面使用$ 所取得的值,被称为投影属性

修改之后ContentView.swift的代码为如下:

//
//  ContentView.swift
//  Calculator
//
//  Created by wei on 2021/7/1.
//

import SwiftUI

///按钮宽度
let size_width = 88

let scale = UIScreen.main.bounds.width / 414


///主内容视图
struct ContentView: View {

    //state 在swiftui中会自动转换为一对 setter 和 getter. 对这个属性进行赋值
    //的操作将会触发 view的刷新
    @State private var brain: CalculatorBrain = .left("0")

    var body: some View {
        //垂直布局
        VStack(spacing: 12, content: {
            Spacer() //为了填充上面的留白
            Text(brain.output) //输入显示的区域
                .font(.system(size: 76))
                .minimumScaleFactor(0.5)
                .padding(.trailing, 24 * scale)
                .frame(
                    minWidth: 0,
                    maxWidth: .infinity,  //最大
                    alignment: .trailing  //靠右
                )
            Button("Test") {
                self.brain = .left("1.23")
            }
            CalculatorButtonPad(brain: $brain)  //计算器上面的按键
                .padding(.bottom)  //底部留白为0
        })
    }
}


///效果显示调用的地方
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


///每个button的封装
struct CalculatorButton : View {

    let fontSize: CGFloat = 38
    let title: String
    let size: CGSize
    let backgroundColorName: String
    let action: ()->Void

    var body: some View {
        Button(action: action, label: {
            Text(title)
                .font(.system(size: fontSize)) //字体
                .foregroundColor(.blue)  //字色
                .frame(width: size.width, height: size.height) //size
                .background(Color(backgroundColorName)) //背景色
                .cornerRadius(size.width/2) //圆角
        })
    }
}


/// 封装的每一行 ,使用foreach
struct CalculatorButtonRow: View {

    @Binding var brain: CalculatorBrain

    let row: [CalculatorButtonItem] //每一行里面的元素button

    var body: some View {
        HStack{  //水平布局
            ForEach(row, id: \.self) { item in  //使用foreach水平布局button
                CalculatorButton(
                    title: item.title,
                    size: item.size,
                    backgroundColorName: item.backgroundColorName) {
                    print("button : \(item.title)")
                    self.brain = self.brain.apply(item: item)
                }
            }
        }
    }
}

/// 封装计算器上的所有按键
struct CalculatorButtonPad: View {

    @Binding var brain: CalculatorBrain

    /// pad里面是每一行的数据,具体意思看下item类
    let pad: [[CalculatorButtonItem]] = [
        [.command(.clear), .command(.flip), .command(.percent), .op(.divide)],
        [.digit(7), .digit(8), .digit(9), .op(.multipy)],
        [.digit(4), .digit(5), .digit(6), .op(.minus)],
        [.digit(1), .digit(2), .digit(3), .op(.plus)],
        [.digit(0), .dot, .op(.equal)],
    ]

    var body: some View {
        //垂直布局类
        VStack(spacing: 8, content: {
            //使用foreach添加每一行
            ForEach(pad, id: \.self) { row in
                CalculatorButtonRow(brain: self.$brain, row: row)//每一行的数据
            }
        })
    }
}

效果:

2021-07-08 10.48.41.gif

以上代码来自猫神swiftUI 和 combine 学习笔记