diff --git a/Learning SwiftUI/Learning SwiftUI.xcodeproj/project.pbxproj b/Learning SwiftUI/Learning SwiftUI.xcodeproj/project.pbxproj index 0c0ceb4..dc79faa 100644 --- a/Learning SwiftUI/Learning SwiftUI.xcodeproj/project.pbxproj +++ b/Learning SwiftUI/Learning SwiftUI.xcodeproj/project.pbxproj @@ -14,7 +14,9 @@ 7744737724C076F200614A17 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7744737624C076F200614A17 /* Preview Assets.xcassets */; }; 7744737A24C076F200614A17 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7744737824C076F200614A17 /* LaunchScreen.storyboard */; }; 77E2340B24CEC6C00085EDD7 /* CalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E2340A24CEC6C00085EDD7 /* CalculatorView.swift */; }; - 77E2340D24CEC6C80085EDD7 /* CalculatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E2340C24CEC6C80085EDD7 /* CalculatorModel.swift */; }; + 77E2340D24CEC6C80085EDD7 /* CalculatorButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E2340C24CEC6C80085EDD7 /* CalculatorButtonItem.swift */; }; + 77E2344324D01DF90085EDD7 /* CalculatorBrain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E2344224D01DF90085EDD7 /* CalculatorBrain.swift */; }; + 77E2344524D03C9D0085EDD7 /* CalculatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E2344424D03C9D0085EDD7 /* CalculatorModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -27,7 +29,9 @@ 7744737924C076F200614A17 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7744737B24C076F200614A17 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 77E2340A24CEC6C00085EDD7 /* CalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorView.swift; sourceTree = ""; }; - 77E2340C24CEC6C80085EDD7 /* CalculatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorModel.swift; sourceTree = ""; }; + 77E2340C24CEC6C80085EDD7 /* CalculatorButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorButtonItem.swift; sourceTree = ""; }; + 77E2344224D01DF90085EDD7 /* CalculatorBrain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorBrain.swift; sourceTree = ""; }; + 77E2344424D03C9D0085EDD7 /* CalculatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -91,7 +95,9 @@ 77E2340924CEC6A60085EDD7 /* Calculator */ = { isa = PBXGroup; children = ( - 77E2340C24CEC6C80085EDD7 /* CalculatorModel.swift */, + 77E2344224D01DF90085EDD7 /* CalculatorBrain.swift */, + 77E2340C24CEC6C80085EDD7 /* CalculatorButtonItem.swift */, + 77E2344424D03C9D0085EDD7 /* CalculatorModel.swift */, 77E2340A24CEC6C00085EDD7 /* CalculatorView.swift */, ); path = Calculator; @@ -168,11 +174,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 77E2344324D01DF90085EDD7 /* CalculatorBrain.swift in Sources */, 77E2340B24CEC6C00085EDD7 /* CalculatorView.swift in Sources */, - 77E2340D24CEC6C80085EDD7 /* CalculatorModel.swift in Sources */, + 77E2340D24CEC6C80085EDD7 /* CalculatorButtonItem.swift in Sources */, 7744736E24C076F200614A17 /* AppDelegate.swift in Sources */, 7744737024C076F200614A17 /* SceneDelegate.swift in Sources */, 7744737224C076F200614A17 /* ContentView.swift in Sources */, + 77E2344524D03C9D0085EDD7 /* CalculatorModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorBrain.swift b/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorBrain.swift new file mode 100644 index 0000000..b8072fe --- /dev/null +++ b/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorBrain.swift @@ -0,0 +1,233 @@ +// +// CalculatorBrain.swift +// Learning SwiftUI +// +// Created by Ezreal on 2020/7/28. +// Copyright © 2020 Ezeal. All rights reserved. +// + +import Foundation + +// MARK: - 计算器的状态 + +/// 左侧数字 + 计算符号 + 右侧数字 + 计算符号或等号 +enum CalculatorBrain { + /// 计算器正在输入算式左侧数字,这个状态将在用户按下计算操作按钮 (加减乘除号) 后改变为下一个状态。 + case left(String) + /// 计算器输入了左侧数字和计算符号,等待开始输入右侧符号。 + case leftOp( + left: String, + op: CalculatorButtonItem.Op + ) + /// 计算器已经输入了左侧数字,计算符号,和部分右侧数字,并在等待更多右侧数字的输入。 + case leftOpRight( + left: String, + op: CalculatorButtonItem.Op, + right: String + ) + case equal(value: String) + /// 输入或计算结果出现了错误,无法继续。比如发生了“除以 0”的操作。 + case error + + /// 用于显示结果 + var output: String { + let result: String + switch self { + case .left(let left): result = left + case .leftOp(let left, _): result = left + case .leftOpRight(_, _, let right): result = right + case .equal(value: let value): result = value + case .error: result = "Error" + } + guard let value = Double(result) else { + return "Error" + } + + return formatter.string(from: value as NSNumber)! + } + + /// 接收输入数据,并返回计算器当前状态 + func apply(item: CalculatorButtonItem) -> CalculatorBrain { + switch item { + case .digit(let num): return apply(num: num) + case .dot: return applyDot() + case .op(let op): return apply(op: op) + case .command(let command): return apply(command: command) + } + } +} + +/// 8位小数点以内格式化 +fileprivate var formatter: NumberFormatter = { + let f = NumberFormatter() + f.minimumFractionDigits = 0 + f.maximumFractionDigits = 8 + f.numberStyle = .decimal + return f +}() + +private extension CalculatorBrain { + + func apply(num: Int) -> CalculatorBrain { + switch self { + case .left(let left): + return .left(left.apply(num: num)) + case .leftOp(let left, let op): + return .leftOpRight(left: left, op: op, right: "0".apply(num: num)) + case .leftOpRight(let left, let op, let right): + return .leftOpRight(left: left, op: op, right: right.apply(num: num)) + case .equal(_): + return .left("0".apply(num: num)) + case .error: + return .left("0".apply(num: num)) + } + } + + func applyDot() -> CalculatorBrain { + switch self { + case .left(let left): + return .left(left.applyDot()) + case .leftOp(let left, let op): + return .leftOpRight(left: left, op: op, right: "0".applyDot()) + case .leftOpRight(let left, let op, let right): + return .leftOpRight(left: left, op: op, right: right.applyDot()) + case .equal(_): + return .left("0".applyDot()) + case .error: + return .left("0".applyDot()) + } + } + + func apply(op: CalculatorButtonItem.Op) -> CalculatorBrain { + switch self { + case .left(let left): + switch op { + case .plus, .minus, .multiply, .divide: + return .leftOp(left: left, op: op) + case .equal: + return self + } + case .leftOp(let left, let currentOp): + switch op { + case .plus, .minus, .multiply, .divide: + return .leftOp(left: left, op: op) + case .equal: + if let result = currentOp.calculate(l: left, r: left) { + return .leftOp(left: result, op: currentOp) + } else { + return .error + } + } + case .leftOpRight(let left, let currentOp, let right): + switch op { + case .plus, .minus, .multiply, .divide: + if let result = currentOp.calculate(l: left, r: right) { + return .leftOp(left: result, op: currentOp) + } else { + return .error + } + case .equal: + if let result = currentOp.calculate(l: left, r: right) { + return .equal(value: result) + } else { + return .error + } + } + case .equal(let value): + switch op { + case .plus, .minus, .multiply, .divide: + return .leftOp(left: value, op: op) + case .equal: return self + } + case .error: + return self + } + } + + func apply(command: CalculatorButtonItem.Command) -> CalculatorBrain { + switch command { + case .clear: + return .left("0") + case .flip: + switch self { + case .left(let left): + return .left(left.flipped()) + case .leftOp(let left, let op): + return .leftOpRight(left: left, op: op, right: "-0") + case .leftOpRight(let left, let op, let right): + return .leftOpRight(left: left, op: op, right: right.flipped()) + case .equal(let value): + return .left(value.flipped()) + case .error: + return .left("-0") + } + case .percent: + switch self { + case .left(let left): + return .left(left.percentaged()) + case .leftOp: + return self + case .leftOpRight(let left, let op, let right): + return .leftOpRight(left: left, op: op, right: right.percentaged()) + case .equal(let value): + return .left(value.percentaged()) + case .error: + return .left("-0") + } + } + } +} + +// MARK: - 字符串功能拓展 + +extension String { + + /// 是否含有小数点 + var containsDot: Bool { contains(".") } + + /// 是否为负数 + var startWithNegative: Bool { starts(with: "-") } + + /// 输入数值 + func apply(num: Int) -> String { self == "0" ? "\(num)" : "\(self)\(num)" } + + /// 添加小数点 + func applyDot() -> String { containsDot ? self : "\(self)." } + + /// 取负值 + func flipped() -> String { + if startWithNegative { + var s = self + s.removeFirst() + return s + } else { + return "-\(self)" + } + } + + /// 百分比 + func percentaged() -> String { String(Double(self)! / 100) } +} + +// MARK: - 运算符的计算方法 + +extension CalculatorButtonItem.Op { + + func calculate(l: String, r: String) -> String? { + + guard let left = Double(l), let right = Double(r) else { + return nil + } + + let result: Double? + switch self { + case .plus: result = left + right + case .minus: result = left - right + case .multiply: result = left * right + case .divide: result = right == 0 ? nil : left / right + case .equal: fatalError("等号不可能为连接运算符") + } + + return result.map { String($0) } + } +} diff --git a/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorButtonItem.swift b/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorButtonItem.swift new file mode 100644 index 0000000..0fa2b1a --- /dev/null +++ b/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorButtonItem.swift @@ -0,0 +1,79 @@ +// +// CalculatorModel.swift +// Learning SwiftUI +// +// Created by Ezreal on 2020/7/27. +// Copyright © 2020 Ezeal. All rights reserved. +// + +import Foundation +import UIKit + +// MARK: - 按钮模型 + +enum CalculatorButtonItem { + + /// 运算符 + enum Op: String { + case plus = "+" + case minus = "-" + case divide = "÷" + case multiply = "×" + case equal = "=" + } + + /// 指令 + enum Command: String { + case clear = "AC" + case flip = "+/-" + case percent = "%" + } + + case digit(Int) + case dot + case op(Op) + case command(Command) +} + +extension CalculatorButtonItem { + + var title: String { + switch self { + case .digit(let value): return String(value) + case .dot: return "." + case .op(let op): return op.rawValue + case .command(let command): return command.rawValue + } + } + + var size: CGSize { + if case .digit(let value) = self, value == 0 { + return CGSize(width: 88 * 2 + 8, height: 88) + } + return CGSize(width: 88, height: 88) + } + + var backgroundColorName: String { + switch self { + case .digit, .dot: return "digitBackground" + case .op: return "operatorBackground" + case .command: return "commandBackground" + } + } +} + +/// rawValue 作为 ForEach.id 的要求 Hashable +extension CalculatorButtonItem: Hashable {} + +/// 为枚举对象添加描述,"\(CalculatorButtonItem)"便于显示 +extension CalculatorButtonItem: CustomStringConvertible { + + var description: String { + switch self { + case .digit(let num): return String(num) + case .dot: return "." + case .op(let op): return op.rawValue + case .command(let command): return command.rawValue + } + } +} diff --git a/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorModel.swift b/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorModel.swift index 24415cc..5b2e6b6 100644 --- a/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorModel.swift +++ b/Learning SwiftUI/Learning SwiftUI/Calculator/CalculatorModel.swift @@ -2,83 +2,48 @@ // CalculatorModel.swift // Learning SwiftUI // -// Created by Ezreal on 2020/7/27. +// Created by Ezreal on 2020/7/28. // Copyright © 2020 Ezeal. All rights reserved. // -import Foundation -import UIKit +import Combine -// MARK: - 按钮模型 - -enum CalculatorButtonItem: Hashable {// rawValue 作为 ForEach.id 的要求 Hashable +class CalculatorModel: ObservableObject { - /// 运算符 - enum Op: String { - case plus = "+" - case minus = "-" - case divide = "÷" - case multiply = "×" - case equal = "=" - } + @Published var brain: CalculatorBrain = .left("0") + @Published var history: [CalculatorButtonItem] = [] + + var temporaryKept: [CalculatorButtonItem] = [] - /// 指令 - enum Command: String { - case clear = "AC" - case flip = "+/-" - case percent = "%" + var historyDetail: String { + return history.map{ $0.description }.joined() } - case digit(Int) - case dot - case op(Op) - case command(Command) -} + var totalCount: Int { + history.count + temporaryKept.count + } -extension CalculatorButtonItem { - - var title: String { - switch self { - case .digit(let value): return String(value) - case .dot: return "." - case .op(let op): return op.rawValue - case .command(let command): return command.rawValue + var slidingIndex: Float = 0 { + didSet { + keepHistory(upTo: Int(slidingIndex)) } } - var size: CGSize { - if case .digit(let value) = self, value == 0 { - return CGSize(width: 88 * 2 + 8, height: 88) - } - return CGSize(width: 88, height: 88) + func apply(_ item: CalculatorButtonItem) { + brain = brain.apply(item: item) + history.append(item) + temporaryKept.removeAll() + slidingIndex = Float(totalCount) } - var backgroundColorName: String { - switch self { - case .digit, .dot: return "digitBackground" - case .op: return "operatorBackground" - case .command: return "commandBackground" - } + func keepHistory(upTo index: Int) { + precondition(index <= totalCount, "Out of index.") + + let total = history + temporaryKept + + history = Array(total[.. CalculatorButton in + ForEach(items, id: \.self) { item in CalculatorButton(title: item.title, size: item.size, backgroundColorName: item.backgroundColorName, - action: { print(item.title) }) + action: { self.model.apply(item) }) } } } @@ -51,7 +54,7 @@ struct CalculatorButtonRow: View { // MARK: - 计算器列 struct CalculatorButtonPad: View { - + let data: [[CalculatorButtonItem]] = [[.command(.clear), .command(.flip), .command(.percent), .op(.divide)], [.digit(7), .digit(8), .digit(9), .op(.multiply)], [.digit(4), .digit(5), .digit(6), .op(.minus)], @@ -61,9 +64,7 @@ struct CalculatorButtonPad: View { var body: some View { VStack(spacing: 8) { - ForEach(data, id: \.self) { (item) -> CalculatorButtonRow in - CalculatorButtonRow(items: item) - } + ForEach(data, id: \.self) { CalculatorButtonRow(items: $0) } } } } @@ -72,13 +73,20 @@ struct CalculatorButtonPad: View { struct CalculatorView: View { - let scale: CGFloat = UIScreen.main.bounds.width / 414.0 + @EnvironmentObject var model: CalculatorModel + @State private var editingHistory = false + var body: some View { VStack(spacing: 12) { Spacer() - Text("0") + Button("操作履历:\(model.history.count)") { + self.editingHistory = true + }.sheet(isPresented: self.$editingHistory) { + HistoryView(model: self.model) + } + Text(model.brain.output) .font(.system(size: 76)) .minimumScaleFactor(0.5) .padding(.trailing, 24) @@ -87,6 +95,33 @@ struct CalculatorView: View { CalculatorButtonPad() .padding(.bottom) } - .scaleEffect(scale) + .scaleEffect(UIScreen.main.bounds.width / 414.0) + } +} + +// MARK: - 历史记录 + +struct HistoryView: View { + + @ObservedObject var model: CalculatorModel + + var body: some View { + VStack { + if model.totalCount == 0 { + Text("没有履历") + } else { + HStack { + Text("履历").font(.headline) + Text("\(model.historyDetail)").lineLimit(nil) + } + HStack { + Text("显示").font(.headline) + Text("\(model.brain.output)") + } + Slider(value: $model.slidingIndex, + in: 0...Float(model.totalCount), + step: 1) + } + } } } diff --git a/Learning SwiftUI/Learning SwiftUI/ContentView.swift b/Learning SwiftUI/Learning SwiftUI/ContentView.swift index 81b2bc6..a478343 100644 --- a/Learning SwiftUI/Learning SwiftUI/ContentView.swift +++ b/Learning SwiftUI/Learning SwiftUI/ContentView.swift @@ -12,7 +12,7 @@ struct ContentView: View { var body: some View { - CalculatorView() + CalculatorView().environmentObject(CalculatorModel()) } } @@ -21,9 +21,9 @@ struct ContentView_Previews: PreviewProvider { static var previews: some View { Group { ContentView() - ContentView().previewDevice("iPhone SE") - ContentView().previewDevice("iPhone 8 Plus") - ContentView().previewDevice("iPhone Xs Max") +// ContentView().previewDevice("iPhone SE") +// ContentView().previewDevice("iPhone 8 Plus") +// ContentView().previewDevice("iPhone Xs Max") } } }