diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj b/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj index 2dafd1d..4d8887f 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj @@ -37,7 +37,7 @@ 7151D4102CE5970500620914 /* Exceptions for "CalculatorApp-Codebase" folder in "CalculatorApp-Codebase" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( - Info.plist, + App/Info.plist, ); target = 7151D3E72CE5970400620914 /* CalculatorApp-Codebase */; }; @@ -307,11 +307,13 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "CalculatorApp-Codebase/Info.plist"; + INFOPLIST_FILE = "CalculatorApp-Codebase/App/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -321,7 +323,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -333,11 +335,13 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "CalculatorApp-Codebase/Info.plist"; + INFOPLIST_FILE = "CalculatorApp-Codebase/App/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -347,7 +351,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/AppDelegate.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift similarity index 77% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/AppDelegate.swift rename to CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift index 10e9974..033f539 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/AppDelegate.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift @@ -9,28 +9,34 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - - - + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } - + + /// Specifies the supported interface orientations for the application. + /// - Note: This method restricts the app to portrait orientation only. + func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + return .portrait // Allow Only Portrait Mode + } + // MARK: UISceneSession Lifecycle - + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - - + + } diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Base.lproj/LaunchScreen.storyboard b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/Base.lproj/LaunchScreen.storyboard rename to CalculatorApp-Codebase/CalculatorApp-Codebase/App/Base.lproj/LaunchScreen.storyboard diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Info.plist b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/Info.plist similarity index 100% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/Info.plist rename to CalculatorApp-Codebase/CalculatorApp-Codebase/App/Info.plist diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/SceneDelegate.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/SceneDelegate.swift similarity index 100% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/SceneDelegate.swift rename to CalculatorApp-Codebase/CalculatorApp-Codebase/App/SceneDelegate.swift diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift new file mode 100644 index 0000000..377d6f1 --- /dev/null +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift @@ -0,0 +1,142 @@ +// +// CalculatorLogic.swift +// CalculatorApp-Codebase +// +// Created by t0000-m0112 on 2024-11-20. +// + +import UIKit + +// MARK: CalculatorLogic Class +/// Handles all the calculation logic, including managing expressions and results. +class CalculatorLogic { + weak var delegate: CalculatorLogicDelegate? + + // MARK: Properties + private var isLastInputOperator = false + private var isLastInputZero = true + private var expression = "0" { + didSet { + delegate?.didUpdateExpression(expression) + } + } +} + +// MARK: - Input Handler +/// Handles input actions and updates the expression accordingly. +extension CalculatorLogic { + internal func buttonAction(from sender: UIButton) { + guard let buttonTitle = sender.titleLabel?.text else { return } + + switch buttonTitle { + case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": + handleLeadingZeroIfNeeded() // Input Invalidation: Numbers with starting zero (Case B) + appendNumberToExpression(buttonTitle) + handleFirstZeroIfNeeded() // Input Invalidation: Numbers with starting zero (Case A) + case "+", "-", "×", "÷": + handleLastOperatorIfNeeded() // Input Invalidation: Expressions with duplicated operators + appendOperatorToExpression(buttonTitle) + case "AC": + resetExpression() + case "=": + handleLastOperatorIfNeeded() // Input Invalidation: Expressions with an operator in the end + calculateExpression() + default: + break + } + } +} + +// MARK: - Input Validation (Exception Handling) +/// Validates and corrects invalid input cases like duplicated operators or starting zero. +extension CalculatorLogic { + /// Handles the case where the expression starts with a leading zero (e.g., "0123") - Case A. + /// If the second character in the expression is a number, the leading zero is removed. + private func handleFirstZeroIfNeeded() { + guard self.expression.count > 1 else { return } + if self.expression[expression.startIndex] == "0" && self.expression[expression.index(expression.startIndex, offsetBy: 1)].isNumber { + self.expression.removeFirst() + } + } + + /// Handles the case where the expression has an invalid leading zero (e.g., "+001" or "×03") - Case B. + /// Removes the last zero if it is invalid. + private func handleLeadingZeroIfNeeded() { + guard self.expression.count > 2 else { return } + if isLastInputZero && !self.expression[expression.index(expression.endIndex, offsetBy: -2)].isNumber { + self.expression.removeLast() + } + } + + /// Handles the case where the last input is an operator. + /// If the last character in the expression is an operator, it is removed. + private func handleLastOperatorIfNeeded() { + if isLastInputOperator { + self.expression.removeLast() + } + } + + /// Determines whether handling zero input is necessary. + /// - Parameter input: The current input character. + /// - Returns: A boolean indicating if the input can proceed. + /// - If the input is "0", checks if the last character is "÷" to prevent division by zero. + /// - Updates the `isLastInputZero` flag based on the input. + private func isHandlingZeroInputNeeded(_ input: String) -> Bool { + if input == "0" { + guard self.expression[expression.index(expression.endIndex, offsetBy: -1)] != "÷" else { return false } + self.isLastInputZero = true + } else { + self.isLastInputZero = false + } + return true + } +} + +// MARK: - Expression Modification +/// Modifies the current expression by appending numbers or operators. +extension CalculatorLogic { + internal func appendNumberToExpression(_ input: String) { + guard isHandlingZeroInputNeeded(input) else { return } // Input Invalidation: Expressions that devides with zero + self.expression.append(input) + self.isLastInputOperator = false + } + + internal func appendOperatorToExpression(_ input: String) { + self.expression.append(input) + self.isLastInputOperator = true + self.isLastInputZero = false + } + + internal func resetExpression() { + self.expression = "0" + self.isLastInputOperator = false + self.isLastInputZero = true + } + + internal func calculateExpression() { + if let result = calculate(expression) { + self.expression = String(result) + self.isLastInputOperator = false + self.isLastInputZero = false + } + } +} + +// MARK: - Math Engine +/// Handles the mathematical calculations based on the current expression. +extension CalculatorLogic { + private func calculate(_ expression: String) -> Int? { + let expression = NSExpression(format: changeMathSymbols(expression)) + if let result = expression.expressionValue(with: nil, context: nil) as? Int { + return result + } else { + return nil + } + } + + private func changeMathSymbols(_ expression: String) -> String { + expression + .replacingOccurrences(of: "×", with: "*") + .replacingOccurrences(of: "÷", with: "/") + } +} diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift new file mode 100644 index 0000000..3037a36 --- /dev/null +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift @@ -0,0 +1,14 @@ +// +// CalculatorLogicDelegate.swift.swift +// CalculatorApp-Codebase +// +// Created by t0000-m0112 on 2024-11-20. +// + +// MARK: CalculatorLogicDelegate Protocol +/// A protocol to handle updates from the CalculatorLogic class. +protocol CalculatorLogicDelegate: AnyObject { + /// Called when the expression is updated. + /// - Parameter expression: The updated expression string. + func didUpdateExpression(_ expression: String) +} diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Assets.xcassets/AccentColor.colorset/Contents.json b/CalculatorApp-Codebase/CalculatorApp-Codebase/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/Assets.xcassets/AccentColor.colorset/Contents.json rename to CalculatorApp-Codebase/CalculatorApp-Codebase/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Assets.xcassets/AppIcon.appiconset/Contents.json b/CalculatorApp-Codebase/CalculatorApp-Codebase/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/Assets.xcassets/AppIcon.appiconset/Contents.json rename to CalculatorApp-Codebase/CalculatorApp-Codebase/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Assets.xcassets/Contents.json b/CalculatorApp-Codebase/CalculatorApp-Codebase/Resources/Assets.xcassets/Contents.json similarity index 100% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/Assets.xcassets/Contents.json rename to CalculatorApp-Codebase/CalculatorApp-Codebase/Resources/Assets.xcassets/Contents.json diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController.swift deleted file mode 100644 index b591eb8..0000000 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// ViewController.swift -// CalculatorApp-Codebase -// -// Created by t0000-m0112 on 2024-11-14. -// - -import UIKit -import SnapKit - -class ViewController: UIViewController { - - private enum CalculatorButton { - case number(Int), operation(String), allClear, equal - - var title: String { - switch self { - case .number(let value): return "\(value)" - case .operation(let symbol): return symbol - case .allClear: return "AC" - case .equal: return "=" - } - } - - var backgroundColor: UIColor { - switch self { - case .number: return .systemGray - case .operation: return .systemOrange - case .allClear: return .systemOrange - case .equal: return .systemOrange - } - } - - var button: UIButton { - let button = UIButton() - button.frame.size.height = 80 - button.frame.size.width = 80 - button.layer.cornerRadius = 40 - button.backgroundColor = self.backgroundColor - button.setTitle(self.title, for: .normal) - button.setTitleColor(.white, for: .normal) - button.titleLabel?.font = .boldSystemFont(ofSize: 30) - button.addTarget(self, action: #selector(buttonAction(from:)), for: .touchDown) - return button - } - } - - private let expressionLabel = UILabel() - private let superStack = UIStackView() - private let horizontalStacks: [UIStackView] = (0..<4).map { _ in UIStackView() } - private let button0 = CalculatorButton.number(0).button - private let button1 = CalculatorButton.number(1).button - private let button2 = CalculatorButton.number(2).button - private let button3 = CalculatorButton.number(3).button - private let button4 = CalculatorButton.number(4).button - private let button5 = CalculatorButton.number(5).button - private let button6 = CalculatorButton.number(6).button - private let button7 = CalculatorButton.number(7).button - private let button8 = CalculatorButton.number(8).button - private let button9 = CalculatorButton.number(9).button - private let buttonAdd = CalculatorButton.operation("+").button - private let buttonSubtract = CalculatorButton.operation("-").button - private let buttonMultiply = CalculatorButton.operation("×").button - private let buttonDivide = CalculatorButton.operation("÷").button - private let buttonAllClear = CalculatorButton.allClear.button - private let buttonEqual = CalculatorButton.equal.button - private var expression = "0" { - didSet { // Exception Handling: Expressions that start with 0 - if self.expression.count > 1 && self.expression[expression.startIndex] == "0" { - if expression[expression.index(expression.startIndex, offsetBy: 1)].isNumber { - self.expression.removeFirst() - } - } // Update expressionLabel when value changed - self.expressionLabel.text = self.expression - } - } - - override func viewDidLoad() { - super.viewDidLoad() - configureUI() - } - - private func configureUI() { - view.backgroundColor = .black - configureExpressionLabel() - configureSuperStack() - configureButton() - } - - // Button Actions: - @objc private func buttonAction(from sender: UIButton) { - guard let buttonTitle = sender.titleLabel?.text else { return } - - switch buttonTitle { - case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": - self.expression.append(buttonTitle) - case "+", "-", "×", "÷": - self.expression.append(buttonTitle) - case "AC": - resetExpression() - case "=": - if let result = calculate(expression) { - expression = String(result) - } - default: - break - } - } - - private func resetExpression() { self.expression = "0" } - - private func calculate(_ expression: String) -> Int? { - let expression = NSExpression(format: changeMathSymbols(expression)) - if let result = expression.expressionValue(with: nil, context: nil) as? Int { - return result - } else { - return nil - } - } - - private func changeMathSymbols(_ expression: String) -> String { - expression - .replacingOccurrences(of: "×", with: "*") - .replacingOccurrences(of: "÷", with: "/") - } - - // UI Configurations: - - private func configureExpressionLabel() { - expressionLabel.backgroundColor = .black - expressionLabel.text = "0" - expressionLabel.textColor = .white - expressionLabel.textAlignment = .right - expressionLabel.font = UIFont.boldSystemFont(ofSize: 60) - view.addSubview(expressionLabel) - expressionLabel.snp.makeConstraints { - $0.leading.equalToSuperview().offset(30) - $0.trailing.equalToSuperview().offset(-30) - $0.top.equalToSuperview().offset(200) - $0.height.equalTo(100) - } - } - - private func configureSuperStack() { - superStack.axis = .vertical - superStack.backgroundColor = .black - superStack.spacing = 10 - superStack.distribution = .fillEqually - view.addSubview(superStack) - superStack.snp.makeConstraints { - $0.centerX.equalToSuperview() - $0.top.equalTo(expressionLabel.snp.bottom).offset(60) - $0.width.equalTo(350) - } - - horizontalStacks.forEach { - superStack.addArrangedSubview($0) - setupHorizontalStack(for: $0) - } - } - - private func setupHorizontalStack(for stack: UIStackView) { - stack.axis = .horizontal - stack.spacing = 10 - stack.distribution = .fillEqually - stack.snp.makeConstraints { - $0.height.equalTo(80) - } - } - - private func configureButton() { - [button7, button8, button9, buttonAdd] - .forEach { horizontalStacks[0].addArrangedSubview($0) } - [button4, button5, button6, buttonSubtract] - .forEach { horizontalStacks[1].addArrangedSubview($0) } - [button1, button2, button3, buttonMultiply] - .forEach { horizontalStacks[2].addArrangedSubview($0) } - [buttonAllClear, button0, buttonEqual, buttonDivide] - .forEach { horizontalStacks[3].addArrangedSubview($0) } - } -} - -#Preview { - ViewController() -} diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift new file mode 100644 index 0000000..d4c050d --- /dev/null +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift @@ -0,0 +1,207 @@ +// +// ViewController.swift +// CalculatorApp-Codebase +// +// Created by t0000-m0112 on 2024-11-14. +// + +import UIKit +import SnapKit + +// MARK: ViewController Class +/// This class handles the UI and user interactions for the Calculator app. +/// It integrates `CalculatorLogic` for handling the calculation logic. +class ViewController: UIViewController { + // MARK: Properties + /// Calculator logic instance responsible for calculations and state management. + private let calculatorLogic = CalculatorLogic() + + /// Displays the current calculation expression. + private let expressionLabel = UILabel() + + /// The main stack containing all button rows (horizontal stacks). + private let superButtonStack = UIStackView() + + /// An array of horizontal stacks, each containing buttons for one row. + private let horizontalButtonStacks: [UIStackView] = (0..<4).map { _ in UIStackView() } + + /// Buttons for numbers, operators, and special actions. + private let button0 = CalculatorButton.number(0).button + private let button1 = CalculatorButton.number(1).button + private let button2 = CalculatorButton.number(2).button + private let button3 = CalculatorButton.number(3).button + private let button4 = CalculatorButton.number(4).button + private let button5 = CalculatorButton.number(5).button + private let button6 = CalculatorButton.number(6).button + private let button7 = CalculatorButton.number(7).button + private let button8 = CalculatorButton.number(8).button + private let button9 = CalculatorButton.number(9).button + private let buttonAdd = CalculatorButton.operation("+").button + private let buttonSubtract = CalculatorButton.operation("-").button + private let buttonMultiply = CalculatorButton.operation("×").button + private let buttonDivide = CalculatorButton.operation("÷").button + private let buttonAllClear = CalculatorButton.allClear.button + private let buttonEqual = CalculatorButton.equal.button + + // MARK: Lifecycle Methods + override func viewDidLoad() { + super.viewDidLoad() + calculatorLogic.delegate = self + configureUI() + } +} + +// MARK: - UI Configuration +extension ViewController { + /// Configures the overall UI components of the ViewController. + private func configureUI() { + view.backgroundColor = .black + view.translatesAutoresizingMaskIntoConstraints = false + configureSuperButtonStack() + configureButton() + configureExpressionLabel() + } + + /// Configures the main vertical stack view (superStack) that contains all button rows. + private func configureSuperButtonStack() { + superButtonStack.axis = .vertical + superButtonStack.backgroundColor = .black + superButtonStack.spacing = 10 + superButtonStack.distribution = .fillEqually + view.addSubview(superButtonStack) + superButtonStack.snp.makeConstraints { + $0.width.height.equalTo(350) + $0.centerX.equalToSuperview() + $0.centerY.equalToSuperview().offset(UIScreen.main.bounds.height * 0.175) + $0.bottom.lessThanOrEqualToSuperview().offset(-40) + $0.left.greaterThanOrEqualToSuperview() + $0.right.lessThanOrEqualToSuperview() + } + + // Add horizontal stacks to the super stack + horizontalButtonStacks.forEach { + superButtonStack.addArrangedSubview($0) + setupHorizontalStack(for: $0) + } + } + + /// Configures a horizontal stack view for a given row of buttons. + private func setupHorizontalStack(for stack: UIStackView) { + stack.axis = .horizontal + stack.spacing = 10 + stack.distribution = .fillEqually + stack.snp.makeConstraints { + $0.height.equalTo(80) + } + } + + /// Configures all calculator buttons and places them in the horizontal stacks. + private func configureButton() { + [button7, button8, button9, buttonAdd] + .forEach { horizontalButtonStacks[0].addArrangedSubview($0) } + [button4, button5, button6, buttonSubtract] + .forEach { horizontalButtonStacks[1].addArrangedSubview($0) } + [button1, button2, button3, buttonMultiply] + .forEach { horizontalButtonStacks[2].addArrangedSubview($0) } + [buttonAllClear, button0, buttonEqual, buttonDivide] + .forEach { horizontalButtonStacks[3].addArrangedSubview($0) } + } + + /// Configures the expression label displaying the current input or result. + private func configureExpressionLabel() { + expressionLabel.backgroundColor = .black + expressionLabel.text = "0" + expressionLabel.textColor = .white + expressionLabel.textAlignment = .right + expressionLabel.font = UIFont.boldSystemFont(ofSize: 60) + expressionLabel.adjustsFontSizeToFitWidth = true + expressionLabel.minimumScaleFactor = 0.7 + expressionLabel.numberOfLines = 0 + expressionLabel.baselineAdjustment = .alignBaselines + expressionLabel.lineBreakMode = .byTruncatingHead + view.addSubview(expressionLabel) + expressionLabel.snp.makeConstraints { + $0.right.equalTo(superButtonStack.snp.right).offset(-5) + $0.left.equalTo(superButtonStack.snp.left).offset(5) + $0.bottom.equalTo(superButtonStack.snp.top).offset(-40) + $0.top.greaterThanOrEqualTo(view.snp.top).offset(40) + $0.height.greaterThanOrEqualTo(100) + } + } +} + +// MARK: - Button Configuration and Actions +extension ViewController { + /// Represents different types of calculator buttons. + private enum CalculatorButton { + case number(Int), operation(String), allClear, equal + + /// The display title of the button. + var title: String { + switch self { + case .number(let value): return "\(value)" + case .operation(let symbol): return symbol + case .allClear: return "AC" + case .equal: return "=" + } + } + + /// The background color of the button. + var backgroundColor: UIColor { + switch self { + case .number: return .systemGray + case .operation: return .systemOrange + case .allClear: return .systemOrange + case .equal: return .systemOrange + } + } + + /// Creates and configures a UIButton for this type. + var button: UIButton { + let button = UIButton() + button.frame.size.height = 80 + button.frame.size.width = 80 + button.layer.cornerRadius = 80 / 2 + button.backgroundColor = self.backgroundColor + button.setTitle(self.title, for: .normal) + button.setTitleColor(.white, for: .normal) + button.titleLabel?.font = .boldSystemFont(ofSize: 30) + button.addTarget(self, action: #selector(buttonAction(from:)), for: .touchDown) + button.addTarget(self, action: #selector(buttonAnimation(from:)), for: .touchDown) + return button + } + } + + /// Handles the action when a button is pressed. + @objc private func buttonAction(from sender: UIButton) { + calculatorLogic.buttonAction(from: sender) + } + + /// Adds an animation to the button when it is pressed. + @objc private func buttonAnimation(from sender: UIButton) { + let originalColor = sender.backgroundColor + UIView.animate(withDuration: 0.05, + animations: { + sender.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + sender.backgroundColor = originalColor?.withAlphaComponent(0.9) + }, completion: { _ in + UIView.animate(withDuration: 0.05) { + sender.transform = .identity + sender.backgroundColor = originalColor + } + }) + } +} + +// MARK: - Delegate Implementation +extension ViewController: CalculatorLogicDelegate { + /// Updates the expression label with the latest calculation expression. + internal func didUpdateExpression(_ expression: String) { + self.expressionLabel.text = expression + } +} + +// MARK: - Preview +#Preview { + ViewController() +} diff --git a/README.md b/README.md index 9106619..a752ee7 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,98 @@ -# 계산기 만들기 과제 (Week 3-4) +# 📱 CalculatorApp -Swift와 Xcode를 활용해 간단한 계산기 앱을 개발합니다. 이 과제는 Swift 문법을 바탕으로 Playground에서 구현한 로직을 UI와 통합해 실제 앱으로 구현하는 경험을 목표로 합니다. +This is a project for a basic integer calculator capable of performing four fundamental arithmetic operations: addition, subtraction, multiplication, and division.
-## 📝 협업 규칙 +The project consists of two versions: + 1. CalculatorApp-Storyboard: A Storyboard-based implementation for designing the calculator’s interface visually (Current Version: 0.0.8). + 2. CalculatorApp-Codebase: A code-based implementation using programmatic UI to build the calculator (Current Version: 0.2.0). -### Pull Request 작성 규칙 -1. **형식**: `[레벨] 작업 내용 - 팀원 이름` - - 예: `[Lv_1] 라벨 UI 구현 - 홍길동` -2. **작업 세부 사항**: 구현한 주요 기능과 로직에 대한 요약을 작성합니다. -### 레포지토리 설정 및 브랜치 관리 -1. **Fork로 가져오기**: 각 팀원은 레포지토리를 Fork하여 자신의 개인 레포지토리로 가져옵니다. -2. **브랜치 생성**: Fork한 개인 레포지토리에서 각자의 이름을 딴 브랜치를 생성합니다. -3. **Pull Request**: 각자의 브랜치에서 Pull Request를 생성해 코드 리뷰를 요청합니다. 모든 팀원이 Pull Request에 코멘트를 추가하여 피드백을 제공합니다. -4. **수정 및 Merge**: 피드백을 반영하여 수정한 후, 팀원들의 동의를 얻어 merge를 진행합니다. +## 📅 Project Scope --> 풀 리퀘스트를 한 후 Merge하지 않은채 커밋-푸시를 하면 기존 풀 리퀘스트에 들어가기 때문에 그럴 경우 새로운 브랜치를 만듭니다. (ex. Jamong-Lv1, Jamong-Lv2 ...) +| Developer | Links | Project Timeline | +| -------- | --------------------------------- | ---------------------- | +| DoyleHWorks | [GitHub](https://github.com/DoyleHWorks)
[Velog](https://velog.io/@doylehworks/posts?tag=ProjectCalculatorApp) | 2024-10-14
~ 2024-10-22 | -## 📂 코드 파일 구조 +## 📚 Tech Stacks -- **CalculatorApp**: 프로젝트의 메인 진입점이며, SwiftUI로 구현된 인터페이스를 통해 계산기 앱이 실행됩니다. - - **Main.storyboard**: 앱의 기본 UI 구성과 레이아웃을 설정하는 스토리보드 파일입니다. - - **CalculatorViewController.swift**: 계산기의 주요 기능을 구현한 뷰 컨트롤러 파일입니다. - - **Extensions**: UIView와 UIButton에 필요한 공통 설정 및 기능 확장을 모아둔 파일입니다. - - **Utilities**: 계산 로직을 처리하는 헬퍼 메서드를 포함한 파일로, Swift의 `NSExpression`을 활용한 수식 계산 메서드가 구현되어 있습니다. +
+ + +
+ + +
+ + +
+
-## 🌟 필수 구현 기능 (Levels 1-5) +## 🧮 CalculatorApp-Storyboard 0.0.8 (Deprecated) -- **Level 1**: `UILabel`을 사용해 수식을 표시하는 UI를 구현합니다. -- **Level 2**: `UIStackView`를 이용하여 숫자 및 연산 버튼을 구성하는 가로 스택 뷰를 생성합니다. -- **Level 3**: 세로 스택 뷰로 전체 버튼을 배열하여 계산기의 전반적인 UI를 완성합니다. -- **Level 4**: 연산 버튼의 색상을 오렌지로 설정해 차별화합니다. -- **Level 5**: 버튼을 원형으로 만들기 위해 `cornerRadius` 속성을 조정합니다. +Implemented the basic UI and button actions using Storyboard. -## 💪 도전 구현 기능 (Levels 6-8) +![image](https://github.com/user-attachments/assets/4c6fba3c-4cf3-40c5-983f-ae5cfc953c04) -- **Level 6**: 버튼 클릭 시 라벨에 숫자와 연산 기호가 차례로 표시되도록 구현합니다. -- **Level 7**: `AC` 버튼 클릭 시 초기화되어 기본 값 `0`이 표시되도록 구현합니다. -- **Level 8**: `=` 버튼을 클릭하면 수식이 계산되어 결과가 라벨에 표시되도록 구현합니다. +## 🛠️ CalculatorApp-Codebase 0.2.0 -## 📜 구현 가이드 +### 📂 Folder Organization +``` +CalculatorApp-Codebase/ +├── App/ +│ ├── AppDelegate.swift +│ ├── SceneDelegate.swift +│ ├── Info.plist +│ └── LaunchScreen.storyboard +├── Model/ +│ ├── CalculatorLogic.swift +│ └── CalculatorLogicDelegate.swift +├── ViewController/ +│ └── ViewController.swift +└── Resources/ + └── Assets.xcassets +``` -- **CalculatorViewController.swift** - 각 레벨에 따라 구현된 기능을 `CalculatorViewController.swift` 파일에 추가하여 기본 UI와 로직을 통합합니다. +### 🖼️ App Preview +|![Nov-22-2024 01-47-18](https://github.com/user-attachments/assets/2edc0bcd-15ad-4234-a3e6-61e7f544a915)|![image](https://github.com/user-attachments/assets/09007c81-30b3-427d-849e-81d5ee084491)|![image](https://github.com/user-attachments/assets/1cf328fb-fb97-44d3-b719-76c1ec66f35b)| +|---|---|---| -```swift -func calculate(expression: String) -> Int? { - let expression = NSExpression(format: expression) - if let result = expression.expressionValue(with: nil, context: nil) as? Int { - return result - } else { - return nil - } -} -``` +### 📐 Main Features & Considerations + +#### 🎨 User Interface + +- **Expression Label**: Displays the current input or result. +- **Buttons**: Includes digits (0-9), basic operators (+, -, ×, ÷), an all-clear (AC) button, and an equals (=) button. + +#### 🧮 Calculation Logic -- **버튼 및 라벨 설정** - - 버튼의 색상, 크기, 모양을 설정하고 라벨에 표시될 수식을 업데이트합니다. +- **Input Handling**: Manages user inputs, ensuring valid sequences and preventing errors such as multiple consecutive operators or leading zeros. +- **Expression Evaluation**: Utilizes NSExpression to evaluate mathematical expressions, converting symbols as needed for accurate computation. ---- +#### 🔍 Input Validation (Exception Handling) -## 🎯 목표 +- **Division by Zero**: Prevents invalid operations, such as dividing by zero, by implementing checks before updating the expression. +- **Operator Validation**: Removes redundant or consecutive operators from the expression dynamically, ensuring the calculation logic remains consistent. +- **Zero Handling**: Implements specific rules to handle leading zeros in expressions, such as “0123”, by automatically correcting the input. -- **기한**: 11월 22일 (금) 낮 12시까지 제출 -- **제출물**: 개인 과제 결과물을 GitHub에 올리고 링크를 제출합니다. +#### 🏗️ Architecture: Model + ViewController +- This project imitates the Model-View-Controller (MVC) design pattern: + - **Model**: The CalculatorLogic class encapsulates all business logic, ensuring the UI remains decoupled from the underlying calculation operations. + - **ViewController**: Configures and updates UI components like buttons and labels. -## 🔗 참고 링크 -- [Swift 기초 및 iOS 개발 환경 설정](https://developer.apple.com/swift/) -- [Auto Layout 사용 가이드](https://developer.apple.com/documentation/uikit/auto_layout/) +#### 🧩 Delegate Pattern +- Implements the delegate pattern to communicate between the CalculatorLogic model and the ViewController: + - **CalculatorLogicDelegate**: Notifies the controller (ViewController) of changes in the expression or result. ---- +#### ✨ Additional Considerations +- **Efficient Layout Management**: Utilizes SnapKit for concise and readable Auto Layout constraints, simplifying the layout configuration process. +- **Dynamic Font Scaling**: Ensures the text fits within the label, dynamically adjusting the font size for longer expressions. +- **Button Animations**: Buttons feature a press animation that highlights the user’s interaction. +- **Portrait Mode Only**: The app is locked to portrait orientation, ensuring an optimized user experience and layout for calculator functionality. +- **Dark Content Status Bar**: Configured the status bar to use dark content for better visibility and consistency with the app’s design. +- **Left and Right Constraints**: Used left and right constraints instead of leading and trailing, as the calculator does not need to support right-to-left languages, keeping the layout simple and intuitive. -이번 과제를 통해 UI와 로직의 통합 구현을 연습하고, 협업을 통한 코드 리뷰와 피드백을 통해 더 나은 코드 품질을 만들어 봅시다. +## 📦 How to Install +1. Clone this repository: + ```bash + git clone https://github.com/DoyleHWorks/CalculatorApp.git + ```