From a422f244d94850d79794b678982a59c2de6520bb Mon Sep 17 00:00:00 2001 From: Anthony Ingle Date: Sun, 1 May 2022 23:49:08 -0400 Subject: [PATCH] History is now saved, can be cleared --- Calculator.xcodeproj/project.pbxproj | 4 ++ Calculator/ButtonView.swift | 18 +++---- Calculator/CalculatorView.swift | 33 ++++++++----- Calculator/ContentView.swift | 14 +++++- Calculator/HelperFunctions.swift | 39 ++++----------- Calculator/History.swift | 74 ++++++++++++++++++++++++++++ Calculator/HistoryView.swift | 6 +-- Calculator/SettingsView.swift | 31 ++++++++++-- 8 files changed, 160 insertions(+), 59 deletions(-) create mode 100644 Calculator/History.swift diff --git a/Calculator.xcodeproj/project.pbxproj b/Calculator.xcodeproj/project.pbxproj index 180021c..4d14eb7 100644 --- a/Calculator.xcodeproj/project.pbxproj +++ b/Calculator.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 928D96C8280AA58D00B6CE2E /* CalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928D96C7280AA58D00B6CE2E /* CalculatorView.swift */; }; 928D96CA280AA5E400B6CE2E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928D96C9280AA5E400B6CE2E /* SettingsView.swift */; }; 92AA9002281EEBEA008C9EC8 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92AA9001281EEBEA008C9EC8 /* HistoryView.swift */; }; + 92AA9004281F802C008C9EC8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92AA9003281F802C008C9EC8 /* History.swift */; }; 92B01F4A2809250B0069F3D7 /* HotKey in Frameworks */ = {isa = PBXBuildFile; productRef = 92B01F492809250B0069F3D7 /* HotKey */; }; 92B01F4D280942500069F3D7 /* Expression in Frameworks */ = {isa = PBXBuildFile; productRef = 92B01F4C280942500069F3D7 /* Expression */; }; 92F4476C28089FEE00EC45D5 /* CalculatorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F4476B28089FEE00EC45D5 /* CalculatorApp.swift */; }; @@ -28,6 +29,7 @@ 928D96C7280AA58D00B6CE2E /* CalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorView.swift; sourceTree = ""; }; 928D96C9280AA5E400B6CE2E /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 92AA9001281EEBEA008C9EC8 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; + 92AA9003281F802C008C9EC8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; 92F4476828089FEE00EC45D5 /* Calculator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Calculator.app; sourceTree = BUILT_PRODUCTS_DIR; }; 92F4476B28089FEE00EC45D5 /* CalculatorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorApp.swift; sourceTree = ""; }; 92F4476D28089FEE00EC45D5 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -81,6 +83,7 @@ 92AA9001281EEBEA008C9EC8 /* HistoryView.swift */, 928D96C5280A8F9300B6CE2E /* ButtonView.swift */, 92F447862808C3B000EC45D5 /* HelperFunctions.swift */, + 92AA9003281F802C008C9EC8 /* History.swift */, 92F4476F28089FF100EC45D5 /* Assets.xcassets */, 92F4477428089FF100EC45D5 /* Calculator.entitlements */, 92F4477128089FF100EC45D5 /* Preview Content */, @@ -178,6 +181,7 @@ 928D96CA280AA5E400B6CE2E /* SettingsView.swift in Sources */, 928D96C8280AA58D00B6CE2E /* CalculatorView.swift in Sources */, 928D96C6280A8F9300B6CE2E /* ButtonView.swift in Sources */, + 92AA9004281F802C008C9EC8 /* History.swift in Sources */, 925E9140281D02AC0027E183 /* CustomMacTextView.swift in Sources */, 92F4476E28089FEE00EC45D5 /* ContentView.swift in Sources */, 92F447852808C25800EC45D5 /* AppDelegate.swift in Sources */, diff --git a/Calculator/ButtonView.swift b/Calculator/ButtonView.swift index 3235ca8..ed40163 100644 --- a/Calculator/ButtonView.swift +++ b/Calculator/ButtonView.swift @@ -9,8 +9,8 @@ import SwiftUI struct ButtonView: View { @Binding var expression: String - @Binding var history: [History] @Binding var historyIndex: Int + @EnvironmentObject var historyStore: HistoryStore var body: some View { HStack { @@ -50,15 +50,15 @@ struct ButtonView: View { Button { // check that history array is not empty and history index does not go out of bounds - if (!history.isEmpty && historyIndex < history.count - 1) + if (!historyStore.history.isEmpty && historyIndex < historyStore.history.count - 1) { historyIndex += 1 // if incrementing history, remove the number of characters of previous addition if historyIndex > 0 { - expression.removeLast(history[historyIndex - 1].solution.count) + expression.removeLast(historyStore.history[historyIndex - 1].solution.count) } - expression += history[historyIndex].solution + expression += historyStore.history[historyIndex].solution } } label: { Image(systemName: "arrow.up") @@ -95,17 +95,17 @@ struct ButtonView: View { Button { - if (!history.isEmpty) + if (!historyStore.history.isEmpty) { // check if UP ARROW has been pressed yet if historyIndex != -1 { // if decrementing history, remove the number of characters of previous addition - expression.removeLast(history[historyIndex].solution.count) + expression.removeLast(historyStore.history[historyIndex].solution.count) historyIndex -= 1 // if we are not at the beginning of history, add the next solution in if historyIndex != -1 { - expression += history[historyIndex].solution + expression += historyStore.history[historyIndex].solution } } } @@ -146,9 +146,9 @@ struct ButtonView: View { } func addAnswer() { - if (!history.isEmpty) + if (!historyStore.history.isEmpty) { - expression += history[0].solution + expression += historyStore.history[0].solution } } } diff --git a/Calculator/CalculatorView.swift b/Calculator/CalculatorView.swift index c5eeb8c..a6e316d 100644 --- a/Calculator/CalculatorView.swift +++ b/Calculator/CalculatorView.swift @@ -8,16 +8,16 @@ import SwiftUI struct CalculatorView: View { + @EnvironmentObject var historyStore: HistoryStore @State private var expression: String = "" @State private var solution: Double = 0.0 - @State private var history: [History] = [] @State private var historyIndex: Int = -1 var body: some View { VStack { // A view to show all previous solutions and expressions - HistoryView(expression: $expression, history: $history, historyIndex: $historyIndex) + HistoryView(expression: $expression, historyIndex: $historyIndex) // An NSTextField wrapped as a SwiftUI view CustomMacTextView(placeholderText: "Calculate", text: $expression, @@ -30,15 +30,22 @@ struct CalculatorView: View { // insert item into history withAnimation(.spring()) { let historyItem = History(expression: expression, solution: solution.removeZerosFromEnd()) - history.insert(historyItem, at: 0) - + historyStore.history.insert(historyItem, at: 0) } expression = "" solution = 0 + + // save history + HistoryStore.save(history: historyStore.history) { result in + if case .failure(let error) = result { + fatalError(error.localizedDescription) + } + } } catch { solution = 0 } + }, // On every update of the textfield by keyboard onTextChange: { newExpression in @@ -47,30 +54,30 @@ struct CalculatorView: View { // on UP ARROW key onMoveUp: { // check that history array is not empty and history index does not go out of bounds - if (!history.isEmpty && historyIndex < history.count - 1) + if (!historyStore.history.isEmpty && historyIndex < historyStore.history.count - 1) { historyIndex += 1 // if incrementing history, remove the number of characters of previous addition if historyIndex > 0 { - expression.removeLast(history[historyIndex - 1].solution.count) + expression.removeLast(historyStore.history[historyIndex - 1].solution.count) } - expression += history[historyIndex].solution + expression += historyStore.history[historyIndex].solution } }, // on DOWN ARROW key onMoveDown: { - if (!history.isEmpty) + if (!historyStore.history.isEmpty) { // check if UP ARROW has been pressed yet if historyIndex != -1 { // if decrementing history, remove the number of characters of previous addition - expression.removeLast(history[historyIndex].solution.count) + expression.removeLast(historyStore.history[historyIndex].solution.count) historyIndex -= 1 // if we are not at the beginning of history, add the next solution in if historyIndex != -1 { - expression += history[historyIndex].solution + expression += historyStore.history[historyIndex].solution } } } @@ -81,14 +88,14 @@ struct CalculatorView: View { .onChange(of: expression, perform: { newExpression in // insert previous solution if these operators used first - if !history.isEmpty && + if !historyStore.history.isEmpty && (newExpression == "+" || newExpression == "*" || newExpression == "/" || newExpression == "^" || newExpression == "%") { - expression.insert(contentsOf: history[0].solution, at: expression.startIndex) + expression.insert(contentsOf: historyStore.history[0].solution, at: expression.startIndex) } do { @@ -106,7 +113,7 @@ struct CalculatorView: View { .foregroundColor(.gray) // A view for all the buttons at the bottom - ButtonView(expression: $expression, history: $history, historyIndex: $historyIndex) + ButtonView(expression: $expression, historyIndex: $historyIndex) } } } diff --git a/Calculator/ContentView.swift b/Calculator/ContentView.swift index b4ddeb6..8b0bd98 100644 --- a/Calculator/ContentView.swift +++ b/Calculator/ContentView.swift @@ -9,11 +9,12 @@ import SwiftUI import Expression struct ContentView: View { @State private var showingSettings = false + @StateObject private var store = HistoryStore() var body: some View { VStack { - CalculatorView() + CalculatorView() HStack { Text("Menu Bar Calc") @@ -43,6 +44,17 @@ struct ContentView: View { } .padding([.horizontal, .bottom]) .frame(maxWidth: .infinity, maxHeight: .infinity) + .environmentObject(store) + .onAppear { + HistoryStore.load { result in + switch result { + case .failure(let error): + fatalError(error.localizedDescription) + case .success(let history): + store.history = history + } + } + } } } diff --git a/Calculator/HelperFunctions.swift b/Calculator/HelperFunctions.swift index e697946..7ea5fbb 100644 --- a/Calculator/HelperFunctions.swift +++ b/Calculator/HelperFunctions.swift @@ -8,10 +8,16 @@ import SwiftUI import Expression -struct History: Identifiable { - let id = UUID() - var expression: String - var solution: String +func evaluateExpression(_ givenExpression: String) throws -> Double { + var solution: Double = 0; + + // ability to use ^ as a power operator + let expression = Expression(givenExpression, symbols: [ + .infix("^"): { params in pow(params[0], params[1]) }, + ]) + solution = try expression.evaluate() + + return solution; } extension Double { @@ -25,18 +31,6 @@ extension Double { } } -func evaluateExpression(_ givenExpression: String) throws -> Double { - var solution: Double = 0; - - // ability to use ^ as a power operator - let expression = Expression(givenExpression, symbols: [ - .infix("^"): { params in pow(params[0], params[1]) }, - ]) - solution = try expression.evaluate() - - return solution; -} - struct CalcButtonStyle: ButtonStyle { var foregroundColor: Color var backgroundColor: Color @@ -67,16 +61,3 @@ extension Button { ) } } - -extension View { - - @discardableResult - func openInWindow(title: String, sender: Any?) -> NSWindow { - let controller = NSHostingController(rootView: self) - let win = NSWindow(contentViewController: controller) - win.contentViewController = controller - win.title = title - win.makeKeyAndOrderFront(sender) - return win - } -} diff --git a/Calculator/History.swift b/Calculator/History.swift new file mode 100644 index 0000000..156cf8d --- /dev/null +++ b/Calculator/History.swift @@ -0,0 +1,74 @@ +// +// History.swift +// Calculator +// +// Created by Anthony Ingle on 5/1/22. +// + +import Foundation +import SwiftUI + +struct History: Identifiable, Codable { + let id: UUID + var date: Date + var expression: String + var solution: String + + init(expression: String, solution: String) { + id = UUID() + date = .now + self.expression = expression + self.solution = solution + } +} + +class HistoryStore : ObservableObject { + @Published var history: [History] = [] + + private static func fileURL() throws -> URL { + try FileManager.default.url(for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false) + .appendingPathComponent("history.data") + } + + static func load(completion: @escaping (Result<[History], Error>) -> Void) { + DispatchQueue.global(qos: .background).async { + do { + let fileURL = try fileURL() + guard let file = try? FileHandle(forReadingFrom: fileURL) else { + DispatchQueue.main.async { + completion(.success([])) + } + return + } + let historyData = try JSONDecoder().decode([History].self, from: file.availableData) + DispatchQueue.main.async { + completion(.success(historyData)) + } + } catch { + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + } + + static func save(history: [History], completion: @escaping (Result) -> Void) { + DispatchQueue.global(qos: .background).async { + do { + let data = try JSONEncoder().encode(history) + let outfile = try fileURL() + try data.write(to: outfile) + DispatchQueue.main.async { + completion(.success(history.count)) + } + } catch { + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + } +} diff --git a/Calculator/HistoryView.swift b/Calculator/HistoryView.swift index 547af0e..7e8d656 100644 --- a/Calculator/HistoryView.swift +++ b/Calculator/HistoryView.swift @@ -9,8 +9,8 @@ import SwiftUI struct HistoryView: View { @Binding var expression: String - @Binding var history: [History] @Binding var historyIndex: Int + @EnvironmentObject var historyStore: HistoryStore var body: some View { // this is an upside down scroll view to show history of expressions and solutions @@ -18,7 +18,7 @@ struct HistoryView: View { LazyVStack(alignment: .center, spacing: nil, content: { // this is reversed order since it is flipped - ForEach(history) { item in + ForEach(historyStore.history) { item in VStack { HStack { Text(item.solution) @@ -27,7 +27,7 @@ struct HistoryView: View { .onTapGesture { expression += item.solution } - .foregroundColor(historyIndex != -1 ? (item.id == history[historyIndex].id ? .accentColor : .primary) : .primary) + .foregroundColor(historyIndex != -1 ? (item.id == historyStore.history[historyIndex].id ? .accentColor : .primary) : .primary) // copy solution button Button { diff --git a/Calculator/SettingsView.swift b/Calculator/SettingsView.swift index 4b726ee..30db70f 100644 --- a/Calculator/SettingsView.swift +++ b/Calculator/SettingsView.swift @@ -6,15 +6,38 @@ // import SwiftUI +import AudioToolbox struct SettingsView: View { + @EnvironmentObject var historyStore: HistoryStore + var body: some View { - VStack(alignment: .leading) { - HStack { - Button("Quit Application") { - NSApplication.shared.terminate(nil) + VStack(alignment: .leading, spacing: 10) { + Button { + withAnimation(.easeOut(duration: 0.1)) { + historyStore.history = [] + HistoryStore.save(history: historyStore.history) { result in + if case .failure(let error) = result { + fatalError(error.localizedDescription) + } + AudioServicesPlaySystemSound(0xf) // plays dock item poof sound + } } + } label: { + Image(systemName: "trash") + Text("Clear History") } + .buttonStyle(.plain) + .frame(maxWidth: .infinity) + + Button { + NSApplication.shared.terminate(nil) + } label: { + Image(systemName: "clear") + Text("Quit") + } + .buttonStyle(.plain) + .foregroundColor(.red) } } }