diff --git a/MacCalculator/MacCalculatorApp.swift b/MacCalculator/MacCalculatorApp.swift new file mode 100644 index 00000000..dddf389b --- /dev/null +++ b/MacCalculator/MacCalculatorApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct MacCalculatorApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/MacCalculator/Models/CalculationEngine.swift b/MacCalculator/Models/CalculationEngine.swift new file mode 100644 index 00000000..6fbe4127 --- /dev/null +++ b/MacCalculator/Models/CalculationEngine.swift @@ -0,0 +1,88 @@ +import Foundation + +class CalculationEngine { + + enum AngleMode { + case degrees, radians + } + var angleMode: AngleMode = .degrees + + private let constants: [String: NSNumber] = [ + "pi": NSNumber(value: Double.pi), + "e": NSNumber(value: M_E) + ] + + private var context: NSMutableDictionary { + let context = NSMutableDictionary() + context.addEntries(from: constants) + return context + } + + func evaluate(expression: String) -> Double? { + let sanitizedExpression = expression + .replacingOccurrences(of: "×", with: "*") + .replacingOccurrences(of: "÷", with: "/") + + let expressionObject = NSExpression(format: sanitizedExpression) + if let result = expressionObject.expressionValue(with: nil, context: self.context) as? NSNumber { + return result.doubleValue + } + + return nil + } + + func calculateUnaryOperation(operand: Double, operation: String) -> Double? { + switch operation { + case "x^2": + return pow(operand, 2) + case "x^3": + return pow(operand, 3) + case "e^x": + return exp(operand) + case "10^x": + return pow(10, operand) + case "1/x": + return 1 / operand + case "%": + return operand / 100.0 + case "sin", "cos", "tan": + let operandInRadians = angleMode == .degrees ? operand * .pi / 180 : operand + switch operation { + case "sin": return sin(operandInRadians) + case "cos": return cos(operandInRadians) + case "tan": return tan(operandInRadians) + default: return nil + } + case "asin", "acos", "atan": + // Inverse trig functions return radians + let resultInRadians: Double? = { + switch operation { + case "asin": return asin(operand) + case "acos": return acos(operand) + case "atan": return atan(operand) + default: return nil + } + }() + guard let result = resultInRadians else { return nil } + return angleMode == .degrees ? result * 180 / .pi : result + case "ln": + return log(operand) + case "log10": + return log10(operand) + case "sqrt": + return sqrt(operand) + default: + return nil + } + } + + func calculateFactorial(of number: Double) -> Double? { + guard number >= 0, number.truncatingRemainder(dividingBy: 1) == 0, number <= 20 else { return nil } // Factorial grows fast + if number == 0 { return 1 } + var result: Double = 1 + for i in 1...Int(number) { + result *= Double(i) + } + return result + } +} diff --git a/MacCalculator/Models/UnitConverter.swift b/MacCalculator/Models/UnitConverter.swift new file mode 100644 index 00000000..024d0f7d --- /dev/null +++ b/MacCalculator/Models/UnitConverter.swift @@ -0,0 +1,43 @@ +import Foundation + +struct UnitConverter { + enum Category: String, CaseIterable, Identifiable { + case length = "Length" + case volume = "Volume" + case weight = "Weight" + var id: String { self.rawValue } + } + + static func units(for category: Category) -> [Dimension] { + switch category { + case .length: + return [UnitLength.meters, UnitLength.kilometers, UnitLength.feet, UnitLength.yards, UnitLength.miles, UnitLength.nauticalMiles, UnitLength.inches] + case .volume: + return [UnitVolume.liters, UnitVolume.milliliters, UnitVolume.gallons, UnitVolume.pints, UnitVolume.cups, UnitVolume.fluidOunces] + case .weight: + return [UnitMass.kilograms, UnitMass.grams, UnitMass.pounds, UnitMass.ounces, UnitMass.stones] + } + } + + static func convert(_ value: Double, from fromUnit: Dimension, to toUnit: Dimension) -> Double { + // Create a Measurement object with the input value and unit. + let measurement = Measurement(value: value, unit: fromUnit) + // Convert it to the target unit. + return measurement.converted(to: toUnit).value + } +} + +// Extension to make Dimension identifiable and hashable for use in SwiftUI Pickers. +extension Dimension: Identifiable, Hashable { + public var id: String { + self.symbol + } + + public static func == (lhs: Dimension, rhs: Dimension) -> Bool { + lhs.symbol == rhs.symbol + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(symbol) + } +} diff --git a/MacCalculator/Tests/CalculationEngineTests.swift b/MacCalculator/Tests/CalculationEngineTests.swift new file mode 100644 index 00000000..0534923c --- /dev/null +++ b/MacCalculator/Tests/CalculationEngineTests.swift @@ -0,0 +1,97 @@ +import Foundation + +// In a real project, this would be an XCTestCase subclass. +// import XCTest +// @testable import MacCalculator + +// This file serves as documentation for the tests that should be run +// to ensure the CalculationEngine is working correctly. + +class CalculationEngineTests { + + let engine = CalculationEngine() + + // A helper to run all tests and print the status. + func runAll() { + print("Running CalculationEngineTests...") + testBasicArithmetic() + testOperatorPrecedence() + testUnaryOperations() + testFactorial() + testTrigonometryDegrees() + testTrigonometryRadians() + testInverseTrigonometry() + print("CalculationEngineTests completed.") + } + + private func assertEqual(_ received: T?, _ expected: T, _ message: String) { + if let received = received, received == expected { + print(" ✅ PASS: \(message)") + } else { + print(" ❌ FAIL: \(message) (Expected: \(expected), Received: \(String(describing: received)))") + } + } + + private func assertNil(_ received: T?, _ message: String) { + if received == nil { + print(" ✅ PASS: \(message)") + } else { + print(" ❌ FAIL: \(message) (Expected: nil, Received: \(String(describing: received)))") + } + } + + private func assertApproxEqual(_ received: Double?, _ expected: Double, _ message: String) { + let tolerance = 0.00001 + if let received = received, abs(received - expected) < tolerance { + print(" ✅ PASS: \(message)") + } else { + print(" ❌ FAIL: \(message) (Expected: \(expected), Received: \(String(describing: received)))") + } + } + + func testBasicArithmetic() { + assertEqual(engine.evaluate(expression: "2 + 3"), 5.0, "Addition") + assertEqual(engine.evaluate(expression: "10 - 5.5"), 4.5, "Subtraction") + assertEqual(engine.evaluate(expression: "4 * 8"), 32.0, "Multiplication") + assertEqual(engine.evaluate(expression: "20 / 4"), 5.0, "Division") + } + + func testOperatorPrecedence() { + assertEqual(engine.evaluate(expression: "2 + 3 * 4"), 14.0, "Precedence: 2 + 3 * 4") + assertEqual(engine.evaluate(expression: "(2 + 3) * 4"), 20.0, "Precedence: (2 + 3) * 4") + } + + func testUnaryOperations() { + assertEqual(engine.calculateUnaryOperation(operand: 5, operation: "x^2"), 25.0, "Square") + assertEqual(engine.calculateUnaryOperation(operand: 4, operation: "1/x"), 0.25, "Reciprocal") + assertEqual(engine.calculateUnaryOperation(operand: 16, operation: "sqrt"), 4.0, "Square Root") + } + + func testFactorial() { + assertEqual(engine.calculateFactorial(of: 5), 120.0, "Factorial of 5") + assertEqual(engine.calculateFactorial(of: 0), 1.0, "Factorial of 0") + assertNil(engine.calculateFactorial(of: -1), "Factorial of negative number") + assertNil(engine.calculateFactorial(of: 21), "Factorial of number > 20") + } + + func testTrigonometryDegrees() { + engine.angleMode = .degrees + assertApproxEqual(engine.calculateUnaryOperation(operand: 30, operation: "sin"), 0.5, "sin(30 deg)") + assertApproxEqual(engine.calculateUnaryOperation(operand: 60, operation: "cos"), 0.5, "cos(60 deg)") + assertApproxEqual(engine.calculateUnaryOperation(operand: 45, operation: "tan"), 1.0, "tan(45 deg)") + } + + func testTrigonometryRadians() { + engine.angleMode = .radians + assertApproxEqual(engine.calculateUnaryOperation(operand: .pi / 6, operation: "sin"), 0.5, "sin(pi/6 rad)") + assertApproxEqual(engine.calculateUnaryOperation(operand: .pi / 3, operation: "cos"), 0.5, "cos(pi/3 rad)") + } + + func testInverseTrigonometry() { + engine.angleMode = .degrees + assertApproxEqual(engine.calculateUnaryOperation(operand: 0.5, operation: "asin"), 30.0, "asin(0.5) to degrees") + + engine.angleMode = .radians + assertApproxEqual(engine.calculateUnaryOperation(operand: 0.5, operation: "asin"), .pi / 6, "asin(0.5) to radians") + } +} diff --git a/MacCalculator/ViewModels/CalculatorViewModel.swift b/MacCalculator/ViewModels/CalculatorViewModel.swift new file mode 100644 index 00000000..0bea6913 --- /dev/null +++ b/MacCalculator/ViewModels/CalculatorViewModel.swift @@ -0,0 +1,151 @@ +import Foundation +import Combine + +class CalculatorViewModel: ObservableObject { + @Published var displayText = "0" + @Published var angleMode: CalculationEngine.AngleMode = .degrees { + didSet { + calculationEngine.angleMode = angleMode + } + } + + private var currentExpression = "" + private let calculationEngine = CalculationEngine() + private var hasResult = false + private var isEnteringNumber = true + + func buttonPressed(label: String) { + switch label { + case "0"..."9": + handleDigit(label) + case ".": + handleDecimalPoint() + case "AC": + reset() + case "+/-": + toggleSign() + case "÷", "×", "−", "+": + handleOperator(label) + case "=": + calculateResult() + case "sin", "cos", "tan", "asin", "acos", "atan", "ln", "log10", "sqrt", "x^2", "x^3", "e^x", "10^x", "1/x", "%": + handleUnaryOperation(label) + case "!": + handleFactorial() + case "π": + handleConstant(Double.pi) + case "e": + handleConstant(M_E) + case "Deg", "Rad": + toggleAngleMode() + default: + break + } + } + + private func handleDigit(_ digit: String) { + if !isEnteringNumber || hasResult { + displayText = "0" + isEnteringNumber = true + hasResult = false + } + + if displayText == "0" { + displayText = digit + } else { + displayText += digit + } + } + + private func handleDecimalPoint() { + if isEnteringNumber && !displayText.contains(".") { + displayText += "." + } + } + + private func reset() { + displayText = "0" + currentExpression = "" + isEnteringNumber = true + hasResult = false + } + + private func toggleSign() { + guard let number = Double(displayText) else { return } + displayText = formatResult(-number) + } + + private func handleOperator(_ op: String) { + if isEnteringNumber { + currentExpression += displayText + isEnteringNumber = false + } + // Allow changing operator + if !currentExpression.isEmpty && "+−×÷".contains(currentExpression.last!) { + currentExpression.removeLast(3) + } + currentExpression += " \(op) " + } + + private func calculateResult() { + if isEnteringNumber { + currentExpression += displayText + } + + if let result = calculationEngine.evaluate(expression: currentExpression) { + displayText = formatResult(result) + currentExpression = "" + hasResult = true + isEnteringNumber = false + } else { + displayText = "Error" + currentExpression = "" + hasResult = true + isEnteringNumber = false + } + } + + private func handleUnaryOperation(_ operation: String) { + guard let operand = Double(displayText) else { return } + if let result = calculationEngine.calculateUnaryOperation(operand: operand, operation: operation) { + displayText = formatResult(result) + isEnteringNumber = false + } else { + displayText = "Error" + } + } + + private func handleFactorial() { + guard let operand = Double(displayText) else { return } + if let result = calculationEngine.calculateFactorial(of: operand) { + displayText = formatResult(result) + isEnteringNumber = false + } else { + displayText = "Error" + } + } + + private func handleConstant(_ value: Double) { + displayText = formatResult(value) + isEnteringNumber = true + } + + private func toggleAngleMode() { + angleMode = (angleMode == .degrees) ? .radians : .degrees + } + + private func formatResult(_ result: Double) -> String { + if result.isInfinite || result.isNaN { + return "Error" + } + if result.truncatingRemainder(dividingBy: 1) == 0 { + return String(format: "%.0f", result) + } else { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 8 + formatter.minimumFractionDigits = 0 + return formatter.string(from: NSNumber(value: result))?.trimmingCharacters(in: ["0"]) ?? "\(result)" + } + } +} diff --git a/MacCalculator/ViewModels/DateCalculatorViewModel.swift b/MacCalculator/ViewModels/DateCalculatorViewModel.swift new file mode 100644 index 00000000..95becf42 --- /dev/null +++ b/MacCalculator/ViewModels/DateCalculatorViewModel.swift @@ -0,0 +1,70 @@ +import Foundation + +class DateCalculatorViewModel: ObservableObject { + enum Mode: String, CaseIterable, Identifiable { + case difference = "Difference Between Dates" + case addSubtract = "Add / Subtract" + var id: String { self.rawValue } + } + + @Published var mode: Mode = .difference + + // Properties for Difference mode + @Published var fromDate = Date() { didSet { calculateDifference() } } + @Published var toDate = Date() { didSet { calculateDifference() } } + @Published var differenceResult = "" + + // Properties for Add/Subtract mode + @Published var startDate = Date() { didSet { calculateAddSubtract() } } + @Published var yearsToAdd: Int = 0 { didSet { calculateAddSubtract() } } + @Published var monthsToAdd: Int = 0 { didSet { calculateAddSubtract() } } + @Published var daysToAdd: Int = 0 { didSet { calculateAddSubtract() } } + @Published var addSubtractResult = "" + @Published var operation: Operation = .add { didSet { calculateAddSubtract() } } + + enum Operation { + case add, subtract + } + + init() { + calculateDifference() + calculateAddSubtract() + } + + func calculateDifference() { + let components = Calendar.current.dateComponents([.year, .month, .day], from: fromDate, to: toDate) + + var parts: [String] = [] + if let years = components.year, years != 0 { + parts.append("\(abs(years)) year" + (abs(years) == 1 ? "" : "s")) + } + if let months = components.month, months != 0 { + parts.append("\(abs(months)) month" + (abs(months) == 1 ? "" : "s")) + } + if let days = components.day, days != 0 { + parts.append("\(abs(days)) day" + (abs(days) == 1 ? "" : "s")) + } + + if parts.isEmpty { + differenceResult = "Same dates" + } else { + differenceResult = parts.joined(separator: ", ") + } + } + + func calculateAddSubtract() { + let sign = operation == .add ? 1 : -1 + var dateComponent = DateComponents() + dateComponent.year = yearsToAdd * sign + dateComponent.month = monthsToAdd * sign + dateComponent.day = daysToAdd * sign + + if let futureDate = Calendar.current.date(byAdding: dateComponent, to: startDate) { + let formatter = DateFormatter() + formatter.dateStyle = .full + addSubtractResult = formatter.string(from: futureDate) + } else { + addSubtractResult = "Calculation Error" + } + } +} diff --git a/MacCalculator/ViewModels/UnitConverterViewModel.swift b/MacCalculator/ViewModels/UnitConverterViewModel.swift new file mode 100644 index 00000000..3bd06c2f --- /dev/null +++ b/MacCalculator/ViewModels/UnitConverterViewModel.swift @@ -0,0 +1,72 @@ +import Foundation +import Combine + +class UnitConverterViewModel: ObservableObject { + // The category of units to convert (e.g., Length, Weight) + @Published var selectedCategory: UnitConverter.Category = .length { + didSet { + // When the category changes, update the list of available units. + updateAvailableUnits() + } + } + + // The list of units available for the selected category. + @Published var availableUnits: [Dimension] = [] + + // The unit to convert from. + @Published var fromUnit: Dimension = UnitLength.meters { + didSet { convert() } + } + + // The unit to convert to. + @Published var toUnit: Dimension = UnitLength.feet { + didSet { convert() } + } + + // The value entered by the user. + @Published var inputValue: String = "1" { + didSet { convert() } + } + + // The calculated result of the conversion. + @Published var outputValue: String = "" + + init() { + // Initialize the available units and perform an initial conversion. + updateAvailableUnits() + convert() + } + + /// Updates the list of `availableUnits` based on the `selectedCategory`. + private func updateAvailableUnits() { + availableUnits = UnitConverter.units(for: selectedCategory) + + // Reset the selected units to sensible defaults when the category changes. + if let firstUnit = availableUnits.first { + fromUnit = firstUnit + toUnit = availableUnits.count > 1 ? availableUnits[1] : firstUnit + } + } + + /// Swaps the from and to units and recalculates the conversion. + func swapUnits() { + let temp = fromUnit + fromUnit = toUnit + toUnit = temp + } + + /// Performs the unit conversion and updates the `outputValue`. + private func convert() { + guard let value = Double(inputValue) else { + outputValue = "" + return + } + + let result = UnitConverter.convert(value, from: fromUnit, to: toUnit) + + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 5 + outputValue = formatter.string(from: NSNumber(value: result)) ?? "" + } +} diff --git a/MacCalculator/Views/CalculatorButton.swift b/MacCalculator/Views/CalculatorButton.swift new file mode 100644 index 00000000..4af86f55 --- /dev/null +++ b/MacCalculator/Views/CalculatorButton.swift @@ -0,0 +1,44 @@ +import SwiftUI + +struct CalculatorButton: View { + + enum ButtonColor { + case number + case operation + case function + case scientificFunction + + var backgroundColor: Color { + switch self { + case .number: return Color(white: 0.2) + case .operation: return .orange + case .function: return Color(white: 0.65) + case .scientificFunction: return Color(white: 0.35) + } + } + + var foregroundColor: Color { + switch self { + case .function: return .black + default: return .white + } + } + } + + let label: String + let color: ButtonColor + var width: CGFloat = 80 + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(label) + .font(.system(size: 32).weight(.medium)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame(width: width, height: 80) + .background(color.backgroundColor) + .foregroundColor(color.foregroundColor) + .cornerRadius(40) + } + } +} diff --git a/MacCalculator/Views/ContentView.swift b/MacCalculator/Views/ContentView.swift new file mode 100644 index 00000000..fef4a61b --- /dev/null +++ b/MacCalculator/Views/ContentView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +struct ContentView: View { + // Enum to define the different calculator modes + enum CalculatorMode: String, CaseIterable, Identifiable { + case standard = "Standard" + case scientific = "Scientific" + case date = "Date Calculation" + case converter = "Unit Converter" + + var id: String { self.rawValue } + } + + // State variable to keep track of the selected mode + @State private var selectedMode: CalculatorMode = .standard + + var body: some View { + NavigationView { + // Sidebar for mode selection + List(CalculatorMode.allCases) { mode in + NavigationLink(destination: viewForMode(mode), tag: mode, selection: $selectedMode) { + Text(mode.rawValue) + } + } + .listStyle(SidebarListStyle()) + .frame(minWidth: 180) + + // The content view for the selected mode + viewForMode(selectedMode) + } + .frame(minWidth: 600, minHeight: 400) + } + + // Helper function to return the correct view for the selected mode + @ViewBuilder + private func viewForMode(_ mode: CalculatorMode) -> some View { + switch mode { + case .standard: + StandardCalculatorView() + case .scientific: + ScientificCalculatorView() + case .date: + DateCalculationView() + case .converter: + UnitConverterView() + } + } +} diff --git a/MacCalculator/Views/DateCalculationView.swift b/MacCalculator/Views/DateCalculationView.swift new file mode 100644 index 00000000..65461c98 --- /dev/null +++ b/MacCalculator/Views/DateCalculationView.swift @@ -0,0 +1,84 @@ +import SwiftUI + +struct DateCalculationView: View { + @StateObject private var viewModel = DateCalculatorViewModel() + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + Picker("Calculation Mode", selection: $viewModel.mode) { + ForEach(DateCalculatorViewModel.Mode.allCases) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.horizontal) + .padding(.top) + + if viewModel.mode == .difference { + DifferenceView(viewModel: viewModel) + } else { + AddSubtractView(viewModel: viewModel) + } + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct DifferenceView: View { + @ObservedObject var viewModel: DateCalculatorViewModel + + var body: some View { + Form { + DatePicker("From:", selection: $viewModel.fromDate, displayedComponents: .date) + DatePicker("To:", selection: $viewModel.toDate, displayedComponents: .date) + + Divider().padding(.vertical) + + HStack { + Text("Difference:") + .font(.headline) + Spacer() + Text(viewModel.differenceResult) + .font(.headline) + .fontWeight(.semibold) + } + } + .padding() + } +} + +struct AddSubtractView: View { + @ObservedObject var viewModel: DateCalculatorViewModel + + var body: some View { + Form { + Picker("Operation", selection: $viewModel.operation) { + Text("Add").tag(DateCalculatorViewModel.Operation.add) + Text("Subtract").tag(DateCalculatorViewModel.Operation.subtract) + } + .pickerStyle(SegmentedPickerStyle()) + + DatePicker("From:", selection: $viewModel.startDate, displayedComponents: .date) + + Section(header: Text("Duration").font(.headline)) { + Stepper("Years: \(viewModel.yearsToAdd)", value: $viewModel.yearsToAdd, in: 0...1000) + Stepper("Months: \(viewModel.monthsToAdd)", value: $viewModel.monthsToAdd, in: 0...1000) + Stepper("Days: \(viewModel.daysToAdd)", value: $viewModel.daysToAdd, in: 0...1000) + } + + Divider().padding(.vertical) + + HStack { + Text("Result:") + .font(.headline) + Spacer() + Text(viewModel.addSubtractResult) + .font(.headline) + .fontWeight(.semibold) + } + } + .padding() + } +} diff --git a/MacCalculator/Views/ScientificCalculatorView.swift b/MacCalculator/Views/ScientificCalculatorView.swift new file mode 100644 index 00000000..dd77721d --- /dev/null +++ b/MacCalculator/Views/ScientificCalculatorView.swift @@ -0,0 +1,75 @@ +import SwiftUI + +struct ScientificCalculatorView: View { + @StateObject private var viewModel = CalculatorViewModel() + + // This layout reflects the functions that are actually implemented in the ViewModel. + let buttonLayout: [[String]] = [ + ["x^2", "x^3", "e^x", "AC", "+/-", "%", "÷"], + ["sin", "cos", "tan", "7", "8", "9", "×"], + ["asin", "acos", "atan", "4", "5", "6", "−"], + ["ln", "log10", "sqrt", "1", "2", "3", "+"], + ["!", "π", "e", "Rad", "0", ".", "="] + ] + + var body: some View { + ZStack { + Color.black.ignoresSafeArea() + VStack(spacing: 8) { + Spacer() + // Display + HStack { + Text(viewModel.angleMode == .degrees ? "Deg" : "Rad") + .font(.subheadline) + .foregroundColor(.white) + .padding(.horizontal) + Spacer() + Text(viewModel.displayText) + .foregroundColor(.white) + .font(.system(size: 80, weight: .light)) + .lineLimit(1) + .minimumScaleFactor(0.4) + } + .padding(.horizontal) + + // Buttons + VStack(spacing: 8) { + ForEach(buttonLayout, id: \.self) { row in + HStack(spacing: 8) { + ForEach(row, id: \.self) { label in + CalculatorButton( + label: getDisplayLabel(for: label), + color: buttonColorType(for: label) + ) { + viewModel.buttonPressed(label: label) + } + } + } + } + } + } + .padding(8) + } + } + + private func getDisplayLabel(for label: String) -> String { + if label == "Rad" { + return viewModel.angleMode == .degrees ? "Rad" : "Deg" + } + return label + } + + private func buttonColorType(for label: String) -> CalculatorButton.ButtonColor { + if "0123456789.".contains(label) { + return .number + } + if ["÷", "×", "−", "+", "="].contains(label) { + return .operation + } + if ["AC", "+/-", "%"].contains(label) { + return .function + } + // All other buttons in the new layout are scientific functions. + return .scientificFunction + } +} diff --git a/MacCalculator/Views/StandardCalculatorView.swift b/MacCalculator/Views/StandardCalculatorView.swift new file mode 100644 index 00000000..3dad67d7 --- /dev/null +++ b/MacCalculator/Views/StandardCalculatorView.swift @@ -0,0 +1,65 @@ +import SwiftUI + +struct StandardCalculatorView: View { + @StateObject private var viewModel = CalculatorViewModel() + + let buttonLayout: [[String]] = [ + ["AC", "+/-", "%", "÷"], + ["7", "8", "9", "×"], + ["4", "5", "6", "−"], + ["1", "2", "3", "+"], + ["0", ".", "="] + ] + + var body: some View { + ZStack { + Color.black.ignoresSafeArea() + VStack(spacing: 12) { + Spacer() + // Display + HStack { + Spacer() + Text(viewModel.displayText) + .foregroundColor(.white) + .font(.system(size: 96, weight: .light)) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + .padding() + + // Buttons + VStack(spacing: 12) { + ForEach(buttonLayout, id: \.self) { row in + HStack(spacing: 12) { + ForEach(row, id: \.self) { label in + if label == "0" { + // TODO: Refactor to use GeometryReader for a more adaptive layout. + // This hardcoded width for the zero button is functional but not robust. + CalculatorButton(label: label, color: .number, width: (80 * 2) + 12) { + viewModel.buttonPressed(label: label) + } + } else { + CalculatorButton(label: label, color: buttonColorType(for: label)) { + viewModel.buttonPressed(label: label) + } + } + } + } + } + } + } + .padding(12) + } + } + + private func buttonColorType(for label: String) -> CalculatorButton.ButtonColor { + switch label { + case "AC", "+/-", "%": + return .function + case "÷", "×", "−", "+", "=": + return .operation + default: + return .number + } + } +} diff --git a/MacCalculator/Views/UnitConverterView.swift b/MacCalculator/Views/UnitConverterView.swift new file mode 100644 index 00000000..ae1a13cc --- /dev/null +++ b/MacCalculator/Views/UnitConverterView.swift @@ -0,0 +1,66 @@ +import SwiftUI + +struct UnitConverterView: View { + @StateObject private var viewModel = UnitConverterViewModel() + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + Picker("Category", selection: $viewModel.selectedCategory) { + ForEach(UnitConverter.Category.allCases) { category in + Text(category.rawValue).tag(category) + } + } + .pickerStyle(SegmentedPickerStyle()) + .padding() + + Form { + HStack(spacing: 20) { + // Input Side + VStack { + Text("From").font(.headline) + Picker("From Unit", selection: $viewModel.fromUnit) { + ForEach(viewModel.availableUnits) { unit in + Text(unit.symbol).tag(unit) + } + } + .labelsHidden() + + TextField("Value", text: $viewModel.inputValue) + .font(.system(size: 24)) + .multilineTextAlignment(.center) + .keyboardType(.decimalPad) + .textFieldStyle(RoundedBorderTextFieldStyle()) + } + + // Swap Button + Button(action: { + viewModel.swapUnits() + }) { + Image(systemName: "arrow.left.arrow.right") + .font(.title) + } + .buttonStyle(PlainButtonStyle()) + + // Output Side + VStack { + Text("To").font(.headline) + Picker("To Unit", selection: $viewModel.toUnit) { + ForEach(viewModel.availableUnits) { unit in + Text(unit.symbol).tag(unit) + } + } + .labelsHidden() + + Text(viewModel.outputValue) + .font(.system(size: 24)) + .frame(maxWidth: .infinity, minHeight: 36) + .background(Color(NSColor.windowBackgroundColor)) + .cornerRadius(5) + } + } + } + .padding() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +}