From 21af8a84f1e6fefb260cd219f4debc6cc998ad2d Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Wed, 20 Nov 2024 21:09:21 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Architecture:=20M?= =?UTF-8?q?odel=20/=20ViewController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate Model(CalculatorLogic, CalculatorLogicDelegate) from ViewController --- .../project.pbxproj | 6 +- .../{ => App}/AppDelegate.swift | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../{ => App}/Info.plist | 0 .../{ => App}/SceneDelegate.swift | 0 .../Model/CalculatorLogic.swift | 52 ++++++ .../Model/CalculatorLogicDelegate.swift | 11 ++ .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../{ => ViewController}/ViewController.swift | 155 ++++++++---------- README.md | 139 ++++++++-------- 12 files changed, 205 insertions(+), 158 deletions(-) rename CalculatorApp-Codebase/CalculatorApp-Codebase/{ => App}/AppDelegate.swift (100%) rename CalculatorApp-Codebase/CalculatorApp-Codebase/{ => App}/Base.lproj/LaunchScreen.storyboard (100%) rename CalculatorApp-Codebase/CalculatorApp-Codebase/{ => App}/Info.plist (100%) rename CalculatorApp-Codebase/CalculatorApp-Codebase/{ => App}/SceneDelegate.swift (100%) create mode 100644 CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift create mode 100644 CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift rename CalculatorApp-Codebase/CalculatorApp-Codebase/{ => Resources}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename CalculatorApp-Codebase/CalculatorApp-Codebase/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename CalculatorApp-Codebase/CalculatorApp-Codebase/{ => Resources}/Assets.xcassets/Contents.json (100%) rename CalculatorApp-Codebase/CalculatorApp-Codebase/{ => ViewController}/ViewController.swift (78%) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj b/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj index 2dafd1d..40e9fb9 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,7 +307,7 @@ 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"; @@ -333,7 +333,7 @@ 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"; diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/AppDelegate.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift similarity index 100% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/AppDelegate.swift rename to CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift 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..09ed18a --- /dev/null +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift @@ -0,0 +1,52 @@ +// +// CalculatorLogic.swift +// CalculatorApp-Codebase +// +// Created by t0000-m0112 on 2024-11-20. +// + +import UIKit + +class CalculatorLogic { + weak var delegate: CalculatorLogicDelegate? + + 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 + delegate?.didUpdateExpression(expression) + } + } +} + +extension CalculatorLogic { + internal func resetExpression() { self.expression = "0" } + internal func getExpression() -> String { self.expression } + internal func appendExpression(_ input: String) { self.expression.append(input)} +} + +extension CalculatorLogic { + internal func calculate() { + if let result = calculateExpression(expression) { + self.expression = String(result) + } + } + + private func calculateExpression(_ 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..250afe5 --- /dev/null +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift @@ -0,0 +1,11 @@ +// +// CalculatorLogicDelegate.swift.swift +// CalculatorApp-Codebase +// +// Created by t0000-m0112 on 2024-11-20. +// + +// Send Expression Update Signal from CalculatorLogic to ViewController +protocol CalculatorLogicDelegate: AnyObject { + 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/ViewController.swift similarity index 78% rename from CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController.swift rename to CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift index b591eb8..b6720b8 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift @@ -9,42 +9,7 @@ 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 calculatorLogic = CalculatorLogic() private let expressionLabel = UILabel() private let superStack = UIStackView() private let horizontalStacks: [UIStackView] = (0..<4).map { _ in UIStackView() } @@ -64,19 +29,10 @@ class ViewController: UIViewController { 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() + calculatorLogic.delegate = self configureUI() } @@ -86,46 +42,10 @@ class ViewController: UIViewController { 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: - +// UI Configurations: +extension ViewController { private func configureExpressionLabel() { expressionLabel.backgroundColor = .black expressionLabel.text = "0" @@ -180,6 +100,71 @@ class ViewController: UIViewController { } } +// CalculatorButton: +extension ViewController { + 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 + } + } +} + +// Button Actions: +extension ViewController { + @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": + calculatorLogic.appendExpression(buttonTitle) + case "+", "-", "×", "÷": + calculatorLogic.appendExpression(buttonTitle) + case "AC": + calculatorLogic.resetExpression() + case "=": + calculatorLogic.calculate() + default: + break + } + } +} + +// Retrieve Update from CalculatorLogic +extension ViewController: CalculatorLogicDelegate { + internal func didUpdateExpression(_ expression: String) { + self.expressionLabel.text = expression + } +} + #Preview { ViewController() } diff --git a/README.md b/README.md index 9106619..06c177d 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,73 @@ -# 계산기 만들기 과제 (Week 3-4) - -Swift와 Xcode를 활용해 간단한 계산기 앱을 개발합니다. 이 과제는 Swift 문법을 바탕으로 Playground에서 구현한 로직을 UI와 통합해 실제 앱으로 구현하는 경험을 목표로 합니다. - -## 📝 협업 규칙 - -### Pull Request 작성 규칙 -1. **형식**: `[레벨] 작업 내용 - 팀원 이름` - - 예: `[Lv_1] 라벨 UI 구현 - 홍길동` -2. **작업 세부 사항**: 구현한 주요 기능과 로직에 대한 요약을 작성합니다. - -### 레포지토리 설정 및 브랜치 관리 -1. **Fork로 가져오기**: 각 팀원은 레포지토리를 Fork하여 자신의 개인 레포지토리로 가져옵니다. -2. **브랜치 생성**: Fork한 개인 레포지토리에서 각자의 이름을 딴 브랜치를 생성합니다. -3. **Pull Request**: 각자의 브랜치에서 Pull Request를 생성해 코드 리뷰를 요청합니다. 모든 팀원이 Pull Request에 코멘트를 추가하여 피드백을 제공합니다. -4. **수정 및 Merge**: 피드백을 반영하여 수정한 후, 팀원들의 동의를 얻어 merge를 진행합니다. - --> 풀 리퀘스트를 한 후 Merge하지 않은채 커밋-푸시를 하면 기존 풀 리퀘스트에 들어가기 때문에 그럴 경우 새로운 브랜치를 만듭니다. (ex. Jamong-Lv1, Jamong-Lv2 ...) - -## 📂 코드 파일 구조 - -- **CalculatorApp**: 프로젝트의 메인 진입점이며, SwiftUI로 구현된 인터페이스를 통해 계산기 앱이 실행됩니다. - - **Main.storyboard**: 앱의 기본 UI 구성과 레이아웃을 설정하는 스토리보드 파일입니다. - - **CalculatorViewController.swift**: 계산기의 주요 기능을 구현한 뷰 컨트롤러 파일입니다. - - **Extensions**: UIView와 UIButton에 필요한 공통 설정 및 기능 확장을 모아둔 파일입니다. - - **Utilities**: 계산 로직을 처리하는 헬퍼 메서드를 포함한 파일로, Swift의 `NSExpression`을 활용한 수식 계산 메서드가 구현되어 있습니다. - -## 🌟 필수 구현 기능 (Levels 1-5) - -- **Level 1**: `UILabel`을 사용해 수식을 표시하는 UI를 구현합니다. -- **Level 2**: `UIStackView`를 이용하여 숫자 및 연산 버튼을 구성하는 가로 스택 뷰를 생성합니다. -- **Level 3**: 세로 스택 뷰로 전체 버튼을 배열하여 계산기의 전반적인 UI를 완성합니다. -- **Level 4**: 연산 버튼의 색상을 오렌지로 설정해 차별화합니다. -- **Level 5**: 버튼을 원형으로 만들기 위해 `cornerRadius` 속성을 조정합니다. - -## 💪 도전 구현 기능 (Levels 6-8) - -- **Level 6**: 버튼 클릭 시 라벨에 숫자와 연산 기호가 차례로 표시되도록 구현합니다. -- **Level 7**: `AC` 버튼 클릭 시 초기화되어 기본 값 `0`이 표시되도록 구현합니다. -- **Level 8**: `=` 버튼을 클릭하면 수식이 계산되어 결과가 라벨에 표시되도록 구현합니다. - -## 📜 구현 가이드 - -- **CalculatorViewController.swift** - 각 레벨에 따라 구현된 기능을 `CalculatorViewController.swift` 파일에 추가하여 기본 UI와 로직을 통합합니다. - -```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 - } -} +# CalculatorApp +## Codebase +### Folder Organization +``` +CalculatorApp-Codebase/ +│ +├── App/ +│ ├── AppDelegate.swift +│ ├── SceneDelegate.swift +│ ├── Info.plist +│ └── LaunchScreen.storyboard +│ +├── Model/ +│ └── CalculatorLogic.swift +│ +├── View/ +│ ├── CalculatorButton.swift +│ └── ExpressionLabel.swift +│ +├── Controller/ +│ └── ViewController.swift +│ +└── Resources/ + └── Assets.xcassets ``` -- **버튼 및 라벨 설정** - - 버튼의 색상, 크기, 모양을 설정하고 라벨에 표시될 수식을 업데이트합니다. - ---- - -## 🎯 목표 - -- **기한**: 11월 22일 (금) 낮 12시까지 제출 -- **제출물**: 개인 과제 결과물을 GitHub에 올리고 링크를 제출합니다. - -## 🔗 참고 링크 -- [Swift 기초 및 iOS 개발 환경 설정](https://developer.apple.com/swift/) -- [Auto Layout 사용 가이드](https://developer.apple.com/documentation/uikit/auto_layout/) - --- -이번 과제를 통해 UI와 로직의 통합 구현을 연습하고, 협업을 통한 코드 리뷰와 피드백을 통해 더 나은 코드 품질을 만들어 봅시다. +## Codebase.Ver.0.0.8 +- CalculatorApp-Storyboard Ver.0.0.8 (Lv.8) 파일이 포함됨 +- CalculatorApp-Codebase Ver.0.0.8 (Lv.8) + +### CalculatorApp-Storyboard Ver.0.0.8 (Lv.8) +숫자와 연산자 버튼을 누를 때마다 `expression` 문자열에 추가하고, 이를 화면(expressionLabel)에 표시한다. +"=" 버튼을 누르면 수식을 계산하여 결과를 표시하며, "AC" 버튼으로 초기화한다. +곱셈(`×`)과 나눗셈(`÷`)은 실제 계산을 위해 `*`, `/`로 변환된다. + +![image](https://github.com/user-attachments/assets/4c6fba3c-4cf3-40c5-983f-ae5cfc953c04) + +### CalculatorApp-Codebase Ver.0.0.8 (Lv.8) + +#### �️ **열거형을 활용한 버튼 관리 (`CalculatorButton`)** +- `enum CalculatorButton`으로 버튼의 상태를 정의: + - **숫자 버튼**과 **연산자 버튼**을 동일한 열거형으로 관리. + - 각 버튼의 **타이틀, 배경색** 등을 프로퍼티로 제공. + - **`button` 프로퍼티**를 통해 버튼 인스턴스를 생성하여 일관성 있는 설정 제공. + - **객체 지향 원칙 활용**: 중복되는 버튼 설정(프레임, 색상, 텍스트 스타일)을 열거형 내부에서 한 번만 정의하여 중복 코드 최소화. + +#### � **버튼 액션 처리의 통합적 구현** +- **모든 버튼의 액션을 `buttonAction` 메서드로 통합**: + - `UIButton`의 **타이틀 기반으로 동작 결정** (`titleLabel?.text`). + - **숫자와 연산자 구분 없이 동일한 메서드**에서 처리. + - `switch` 문을 활용하여 각 버튼의 기능(숫자 추가, 연산자 추가, 초기화, 계산) 실행. + - **선언형 접근법**: 버튼마다 개별 메서드를 만드는 대신, 액션 메서드를 하나로 통합하여 유지보수성 강화. + +#### �️ **`didSet`를 활용한 표현식 업데이트** +- **`expression` 프로퍼티에 `didSet` 사용**: + - **상태 변화 감지**: `expression`이 변경될 때마다 라벨에 자동으로 반영. + - **숫자 입력 예외 처리**: `0`으로 시작하는 표현식을 자동 정리(두 번째 문자가 숫자일 경우 첫 번째 `0` 제거). + +#### � **동적 레이아웃 설정 (`SnapKit`)** +- **Stack View**: + - 수직 스택(superStack)을 상위 컨테이너로 두고, 내부에 4개의 **수평 스택**을 배치. + - 각 수평 스택에 버튼 4개씩 배치하여 계산기의 숫자 및 연산자 버튼을 균일하게 정렬. +- **SnapKit의 제약 조건 사용**: + - **수평 스택 간 간격** 및 각 버튼의 크기를 동적으로 조정. + - 화면 크기에 따라 적절한 버튼 배치가 자동 조정되도록 설정. + +#### � **`NSExpression`을 활용한 수식 평가** +- 수식 계산은 `NSExpression`을 활용하여 문자열 수식을 평가: + - 입력된 수식을 단순히 텍스트가 아닌 **실제 수식으로 변환**하여 계산. + - **커스텀 기호(`×`, `÷`)를 표준 수학 기호(`*`, `/`)로 변환**하는 전처리 함수(`changeMathSymbols`) 포함. + - `NSExpression`의 내장 계산 기능을 사용하여 **간결하고 안전한 계산 로직** 구현. From 4309179abf3c61e6a22aed1baea5ae8c458227ba Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Wed, 20 Nov 2024 21:33:37 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=F0=9F=92=AB=20Feature:=20Button=20Animat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewController/ViewController.swift | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift index b6720b8..caaa87b 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift @@ -38,6 +38,7 @@ class ViewController: UIViewController { private func configureUI() { view.backgroundColor = .black + view.translatesAutoresizingMaskIntoConstraints = false configureExpressionLabel() configureSuperStack() configureButton() @@ -100,7 +101,7 @@ extension ViewController { } } -// CalculatorButton: +// CalculatorButton & buttonAction extension ViewController { private enum CalculatorButton { case number(Int), operation(String), allClear, equal @@ -133,13 +134,11 @@ extension ViewController { 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 } } -} - -// Button Actions: -extension ViewController { + @objc private func buttonAction(from sender: UIButton) { guard let buttonTitle = sender.titleLabel?.text else { return } @@ -156,6 +155,20 @@ extension ViewController { break } } + + @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 + } + }) + } } // Retrieve Update from CalculatorLogic From 6b5792ef02bfc80cc7fefd249f3cc5d32244ad6c Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Wed, 20 Nov 2024 22:57:35 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=F0=9F=92=84=20UI:=20Allow=20portrait=20m?= =?UTF-8?q?ode=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/AppDelegate.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift index 10e9974..49c41b8 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift @@ -9,28 +9,32 @@ 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 } - + + 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. } - - + + } From fbdfe3aa248351a891d5204122c4782cc4ea4cd6 Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Wed, 20 Nov 2024 22:59:36 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Chore:=20Delete?= =?UTF-8?q?=20unused=20code=20/=20Add=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete getExpression(), Add comment to CalculatorLogic --- .../CalculatorApp-Codebase/Model/CalculatorLogic.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift index 09ed18a..2b938c2 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift @@ -7,6 +7,7 @@ import UIKit +// Manage and calculate epxression class CalculatorLogic { weak var delegate: CalculatorLogicDelegate? @@ -24,7 +25,6 @@ class CalculatorLogic { extension CalculatorLogic { internal func resetExpression() { self.expression = "0" } - internal func getExpression() -> String { self.expression } internal func appendExpression(_ input: String) { self.expression.append(input)} } From bb493feade196038f2b25513dc57d45e752a0bc4 Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Wed, 20 Nov 2024 23:01:09 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=F0=9F=92=84=20UI:=20Shrink=20font=20size?= =?UTF-8?q?=20of=20label=20when=20long?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalculatorApp-Codebase/ViewController/ViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift index caaa87b..56dba80 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift @@ -53,6 +53,8 @@ extension ViewController { expressionLabel.textColor = .white expressionLabel.textAlignment = .right expressionLabel.font = UIFont.boldSystemFont(ofSize: 60) + expressionLabel.adjustsFontSizeToFitWidth = true + expressionLabel.minimumScaleFactor = 0.4 view.addSubview(expressionLabel) expressionLabel.snp.makeConstraints { $0.leading.equalToSuperview().offset(30) From 0e8d0e226c08f2df1a4e94d28690089ac3f9f121 Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Thu, 21 Nov 2024 13:55:25 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=E2=9C=A8=20Feature:=20Exception=20handli?= =?UTF-8?q?ng=20-=20duplicated=20operators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX Improvement: Overwrite last operator input --- .../Model/CalculatorLogic.swift | 64 ++++++++++++++++--- .../ViewController/ViewController.swift | 15 +---- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift index 2b938c2..5aaaa4b 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift @@ -11,31 +11,79 @@ import UIKit class CalculatorLogic { weak var delegate: CalculatorLogicDelegate? + private var isLastInputOperator = false + private var isCurrentInputOperator = false private var expression = "0" { - didSet { // Exception Handling: Expressions that start with 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 + } + // Update expressionLabel when value changed delegate?.didUpdateExpression(expression) } } + + // Exception Handling: Expressions that has duplicated operators + private func handleLastCharIfNeeded() { + if isLastInputOperator && isCurrentInputOperator { + self.expression.removeLast() + } + } } extension CalculatorLogic { - internal func resetExpression() { self.expression = "0" } - internal func appendExpression(_ input: String) { self.expression.append(input)} + 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": + appendNumberToExpression(buttonTitle) + case "+", "-", "×", "÷": + handleLastCharIfNeeded() + appendOperatorToExpression(buttonTitle) + case "AC": + resetExpression() + case "=": + calculateExpression() + default: + break + } + } } extension CalculatorLogic { - internal func calculate() { - if let result = calculateExpression(expression) { + internal func appendNumberToExpression(_ input: String) { + self.isCurrentInputOperator = false + self.expression.append(input) + self.isLastInputOperator = false + } + + internal func appendOperatorToExpression(_ input: String) { + self.isCurrentInputOperator = true + self.expression.append(input) + self.isLastInputOperator = true + } + + internal func resetExpression() { + self.isCurrentInputOperator = false + self.expression = "0" + self.isLastInputOperator = false + } + + internal func calculateExpression() { + if let result = calculate(expression) { + self.isCurrentInputOperator = false self.expression = String(result) + self.isLastInputOperator = false } } - - private func calculateExpression(_ expression: String) -> Int? { +} + +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 diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift index 56dba80..097b4ff 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift @@ -142,20 +142,7 @@ extension ViewController { } @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": - calculatorLogic.appendExpression(buttonTitle) - case "+", "-", "×", "÷": - calculatorLogic.appendExpression(buttonTitle) - case "AC": - calculatorLogic.resetExpression() - case "=": - calculatorLogic.calculate() - default: - break - } + calculatorLogic.buttonAction(from: sender) } @objc private func buttonAnimation(from sender: UIButton) { From 6fd6b4020ed366f892c3dbd2179fcf3e2c9d3000 Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Thu, 21 Nov 2024 14:18:16 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Separate?= =?UTF-8?q?=20exception=20handling=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Input Validation (exception handling) --- .../Model/CalculatorLogic.swift | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift index 5aaaa4b..6a7356a 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift @@ -15,25 +15,12 @@ class CalculatorLogic { private var isCurrentInputOperator = false 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 delegate?.didUpdateExpression(expression) } } - - // Exception Handling: Expressions that has duplicated operators - private func handleLastCharIfNeeded() { - if isLastInputOperator && isCurrentInputOperator { - self.expression.removeLast() - } - } } +// MARK: - Input Handler extension CalculatorLogic { internal func buttonAction(from sender: UIButton) { guard let buttonTitle = sender.titleLabel?.text else { return } @@ -41,6 +28,7 @@ extension CalculatorLogic { switch buttonTitle { case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": appendNumberToExpression(buttonTitle) + handleFirstZeroIfNeeded() case "+", "-", "×", "÷": handleLastCharIfNeeded() appendOperatorToExpression(buttonTitle) @@ -54,6 +42,24 @@ extension CalculatorLogic { } } +extension CalculatorLogic { + // Expressions with duplicated operators + private func handleLastCharIfNeeded() { + if isLastInputOperator && isCurrentInputOperator { + self.expression.removeLast() + } + } + + // Expressions with starting zero + private func handleFirstZeroIfNeeded() { + if self.expression.count > 1 && self.expression[expression.startIndex] == "0" { + if expression[expression.index(expression.startIndex, offsetBy: 1)].isNumber { + self.expression.removeFirst() + } + } + } +} + extension CalculatorLogic { internal func appendNumberToExpression(_ input: String) { self.isCurrentInputOperator = false From 6a7970d8d8409bad70f2e0a993aee4c2de4e7f7a Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Thu, 21 Nov 2024 14:32:56 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=F0=9F=92=A1=20Comment:=20Add=20comprehen?= =?UTF-8?q?sive=20documentation=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/AppDelegate.swift | 2 + .../Model/CalculatorLogic.swift | 11 ++++- .../Model/CalculatorLogicDelegate.swift | 5 ++- .../ViewController/ViewController.swift | 42 +++++++++++++++---- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift index 49c41b8..033f539 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/App/AppDelegate.swift @@ -17,6 +17,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { 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 } diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift index 6a7356a..3a62d20 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift @@ -7,10 +7,12 @@ import UIKit -// Manage and calculate epxression +// 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 isCurrentInputOperator = false private var expression = "0" { @@ -21,6 +23,7 @@ class CalculatorLogic { } // 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 } @@ -42,6 +45,8 @@ extension CalculatorLogic { } } +// MARK: - Input Validation (Exception Handling) +/// Validates and corrects invalid input cases like duplicated operators or starting zero. extension CalculatorLogic { // Expressions with duplicated operators private func handleLastCharIfNeeded() { @@ -60,6 +65,8 @@ extension CalculatorLogic { } } +// MARK: - Expression Modification +/// Modifies the current expression by appending numbers or operators. extension CalculatorLogic { internal func appendNumberToExpression(_ input: String) { self.isCurrentInputOperator = false @@ -88,6 +95,8 @@ extension CalculatorLogic { } } +// 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)) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift index 250afe5..3037a36 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogicDelegate.swift @@ -5,7 +5,10 @@ // Created by t0000-m0112 on 2024-11-20. // -// Send Expression Update Signal from CalculatorLogic to ViewController +// 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/ViewController/ViewController.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift index 097b4ff..b534cf0 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift @@ -8,11 +8,24 @@ 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 superStack = UIStackView() + + /// An array of horizontal stacks, each containing buttons for one row. private let horizontalStacks: [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 @@ -30,12 +43,17 @@ class ViewController: UIViewController { 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 @@ -43,10 +61,8 @@ class ViewController: UIViewController { configureSuperStack() configureButton() } -} - -// UI Configurations: -extension ViewController { + + /// Configures the expression label displaying the current input or result. private func configureExpressionLabel() { expressionLabel.backgroundColor = .black expressionLabel.text = "0" @@ -64,6 +80,7 @@ extension ViewController { } } + /// Configures the main vertical stack view (superStack) that contains all button rows. private func configureSuperStack() { superStack.axis = .vertical superStack.backgroundColor = .black @@ -76,12 +93,14 @@ extension ViewController { $0.width.equalTo(350) } + // Add horizontal stacks to the super stack horizontalStacks.forEach { superStack.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 @@ -91,6 +110,7 @@ extension ViewController { } } + /// Configures all calculator buttons and places them in the horizontal stacks. private func configureButton() { [button7, button8, button9, buttonAdd] .forEach { horizontalStacks[0].addArrangedSubview($0) } @@ -103,11 +123,13 @@ extension ViewController { } } -// CalculatorButton & buttonAction +// 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)" @@ -117,6 +139,7 @@ extension ViewController { } } + /// The background color of the button. var backgroundColor: UIColor { switch self { case .number: return .systemGray @@ -126,6 +149,7 @@ extension ViewController { } } + /// Creates and configures a UIButton for this type. var button: UIButton { let button = UIButton() button.frame.size.height = 80 @@ -141,10 +165,12 @@ extension ViewController { } } + /// 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, @@ -160,13 +186,15 @@ extension ViewController { } } -// Retrieve Update from CalculatorLogic +// 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() } From 2ee0f77a11ad0162ae03eddd39fc37b4f0f03e0e Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Thu, 21 Nov 2024 15:43:50 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=E2=9C=A8=20Feature:=20Exception=20handli?= =?UTF-8?q?ng=20-=20leading=20zeros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Input Validation: remove leading zeros after operator inputs --- .../Model/CalculatorLogic.swift | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift index 3a62d20..06faf77 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift @@ -14,7 +14,7 @@ class CalculatorLogic { // MARK: Properties private var isLastInputOperator = false - private var isCurrentInputOperator = false + private var isLastInputZero = true private var expression = "0" { didSet { delegate?.didUpdateExpression(expression) @@ -30,14 +30,16 @@ extension CalculatorLogic { switch buttonTitle { case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": + handleLeadingZeroIfNeeded() // Exception Handling: Numbers with starting zero appendNumberToExpression(buttonTitle) - handleFirstZeroIfNeeded() + handleFirstZeroIfNeeded() // Exception Handling: Expressions with starting zero case "+", "-", "×", "÷": - handleLastCharIfNeeded() + handleLastOperatorIfNeeded() // Exception Handling: Expressions with duplicated operators appendOperatorToExpression(buttonTitle) case "AC": resetExpression() case "=": + handleLastOperatorIfNeeded() // Exception Handling: Expressions with an operator in the end calculateExpression() default: break @@ -48,14 +50,12 @@ extension CalculatorLogic { // MARK: - Input Validation (Exception Handling) /// Validates and corrects invalid input cases like duplicated operators or starting zero. extension CalculatorLogic { - // Expressions with duplicated operators - private func handleLastCharIfNeeded() { - if isLastInputOperator && isCurrentInputOperator { + private func handleLastOperatorIfNeeded() { + if isLastInputOperator { self.expression.removeLast() } } - // Expressions with starting zero private func handleFirstZeroIfNeeded() { if self.expression.count > 1 && self.expression[expression.startIndex] == "0" { if expression[expression.index(expression.startIndex, offsetBy: 1)].isNumber { @@ -63,34 +63,45 @@ extension CalculatorLogic { } } } + + private func handleLeadingZeroIfNeeded() { + guard self.expression.count > 2 else { return } + if isLastInputZero && !expression[expression.index(expression.endIndex, offsetBy: -2)].isNumber { + self.expression.removeLast() + } + } } // MARK: - Expression Modification /// Modifies the current expression by appending numbers or operators. extension CalculatorLogic { internal func appendNumberToExpression(_ input: String) { - self.isCurrentInputOperator = false self.expression.append(input) self.isLastInputOperator = false + if input == "0" { + self.isLastInputZero = true + } else { + self.isLastInputZero = false + } } internal func appendOperatorToExpression(_ input: String) { - self.isCurrentInputOperator = true self.expression.append(input) self.isLastInputOperator = true + self.isLastInputZero = false } internal func resetExpression() { - self.isCurrentInputOperator = false self.expression = "0" self.isLastInputOperator = false + self.isLastInputZero = true } internal func calculateExpression() { if let result = calculate(expression) { - self.isCurrentInputOperator = false self.expression = String(result) self.isLastInputOperator = false + self.isLastInputZero = false } } } From febc29b96ef7d7f02c6638cd8318664ac339449b Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Thu, 21 Nov 2024 15:45:05 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=F0=9F=92=84=20UI:=20Improve=20expression?= =?UTF-8?q?Label=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show more text with less limitation --- .../ViewController/ViewController.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift index b534cf0..f64afc9 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift @@ -70,13 +70,17 @@ extension ViewController { expressionLabel.textAlignment = .right expressionLabel.font = UIFont.boldSystemFont(ofSize: 60) expressionLabel.adjustsFontSizeToFitWidth = true - expressionLabel.minimumScaleFactor = 0.4 + expressionLabel.minimumScaleFactor = 0.5 + expressionLabel.numberOfLines = 0 + expressionLabel.baselineAdjustment = .alignBaselines 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) + $0.bottom.equalToSuperview().offset(-574) + $0.top.greaterThanOrEqualToSuperview().offset(50) + $0.height.greaterThanOrEqualTo(100) + $0.height.lessThanOrEqualTo(250) } } From b710e7184184efe054f91403dd1f90d86eadc3b1 Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Thu, 21 Nov 2024 20:01:55 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=F0=9F=92=84=20UI:=20Improve=20Auto=20Lay?= =?UTF-8?q?out=20logics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supports more screen sizes: iPhone SE 3, iPad, ... Layout calculation base changed: from expressionLabel to superButtonStack --- .../ViewController/ViewController.swift | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift index f64afc9..d4c050d 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/ViewController/ViewController.swift @@ -20,10 +20,10 @@ class ViewController: UIViewController { private let expressionLabel = UILabel() /// The main stack containing all button rows (horizontal stacks). - private let superStack = UIStackView() + private let superButtonStack = UIStackView() /// An array of horizontal stacks, each containing buttons for one row. - private let horizontalStacks: [UIStackView] = (0..<4).map { _ in UIStackView() } + private let horizontalButtonStacks: [UIStackView] = (0..<4).map { _ in UIStackView() } /// Buttons for numbers, operators, and special actions. private let button0 = CalculatorButton.number(0).button @@ -57,49 +57,30 @@ extension ViewController { private func configureUI() { view.backgroundColor = .black view.translatesAutoresizingMaskIntoConstraints = false - configureExpressionLabel() - configureSuperStack() + configureSuperButtonStack() configureButton() - } - - /// 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.5 - expressionLabel.numberOfLines = 0 - expressionLabel.baselineAdjustment = .alignBaselines - view.addSubview(expressionLabel) - expressionLabel.snp.makeConstraints { - $0.leading.equalToSuperview().offset(30) - $0.trailing.equalToSuperview().offset(-30) - $0.bottom.equalToSuperview().offset(-574) - $0.top.greaterThanOrEqualToSuperview().offset(50) - $0.height.greaterThanOrEqualTo(100) - $0.height.lessThanOrEqualTo(250) - } + configureExpressionLabel() } /// Configures the main vertical stack view (superStack) that contains all button rows. - private func configureSuperStack() { - superStack.axis = .vertical - superStack.backgroundColor = .black - superStack.spacing = 10 - superStack.distribution = .fillEqually - view.addSubview(superStack) - superStack.snp.makeConstraints { + 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.top.equalTo(expressionLabel.snp.bottom).offset(60) - $0.width.equalTo(350) + $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 - horizontalStacks.forEach { - superStack.addArrangedSubview($0) + horizontalButtonStacks.forEach { + superButtonStack.addArrangedSubview($0) setupHorizontalStack(for: $0) } } @@ -117,13 +98,35 @@ extension ViewController { /// Configures all calculator buttons and places them in the horizontal stacks. private func configureButton() { [button7, button8, button9, buttonAdd] - .forEach { horizontalStacks[0].addArrangedSubview($0) } + .forEach { horizontalButtonStacks[0].addArrangedSubview($0) } [button4, button5, button6, buttonSubtract] - .forEach { horizontalStacks[1].addArrangedSubview($0) } + .forEach { horizontalButtonStacks[1].addArrangedSubview($0) } [button1, button2, button3, buttonMultiply] - .forEach { horizontalStacks[2].addArrangedSubview($0) } + .forEach { horizontalButtonStacks[2].addArrangedSubview($0) } [buttonAllClear, button0, buttonEqual, buttonDivide] - .forEach { horizontalStacks[3].addArrangedSubview($0) } + .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) + } } } @@ -158,7 +161,7 @@ extension ViewController { let button = UIButton() button.frame.size.height = 80 button.frame.size.width = 80 - button.layer.cornerRadius = 40 + button.layer.cornerRadius = 80 / 2 button.backgroundColor = self.backgroundColor button.setTitle(self.title, for: .normal) button.setTitleColor(.white, for: .normal) From 471861277b80fba44ebcf20ad5e4b3902d19c935 Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Thu, 21 Nov 2024 20:05:32 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=F0=9F=94=A7=20Configure:=20Project=20set?= =?UTF-8?q?tings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Targeted Device Family: iPhones Orientation: Portrait mode only UI Status Bar Style: Dark Content --- .../project.pbxproj | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj b/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj index 40e9fb9..4d8887f 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase.xcodeproj/project.pbxproj @@ -310,8 +310,10 @@ 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; }; @@ -336,8 +338,10 @@ 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; }; From ea91e5bdcf583d917f7afebb82497669bfaf3bbd Mon Sep 17 00:00:00 2001 From: DoyleHWorks Date: Thu, 21 Nov 2024 22:49:06 +0900 Subject: [PATCH 13/14] =?UTF-8?q?=E2=9C=A8=20Feature:=20Exception=20handli?= =?UTF-8?q?ng=20-=20divided=20by=20zero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve structure of existing input validations --- .../Model/CalculatorLogic.swift | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift index 06faf77..377d6f1 100644 --- a/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift +++ b/CalculatorApp-Codebase/CalculatorApp-Codebase/Model/CalculatorLogic.swift @@ -30,16 +30,16 @@ extension CalculatorLogic { switch buttonTitle { case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": - handleLeadingZeroIfNeeded() // Exception Handling: Numbers with starting zero + handleLeadingZeroIfNeeded() // Input Invalidation: Numbers with starting zero (Case B) appendNumberToExpression(buttonTitle) - handleFirstZeroIfNeeded() // Exception Handling: Expressions with starting zero + handleFirstZeroIfNeeded() // Input Invalidation: Numbers with starting zero (Case A) case "+", "-", "×", "÷": - handleLastOperatorIfNeeded() // Exception Handling: Expressions with duplicated operators + handleLastOperatorIfNeeded() // Input Invalidation: Expressions with duplicated operators appendOperatorToExpression(buttonTitle) case "AC": resetExpression() case "=": - handleLastOperatorIfNeeded() // Exception Handling: Expressions with an operator in the end + handleLastOperatorIfNeeded() // Input Invalidation: Expressions with an operator in the end calculateExpression() default: break @@ -50,39 +50,55 @@ extension CalculatorLogic { // MARK: - Input Validation (Exception Handling) /// Validates and corrects invalid input cases like duplicated operators or starting zero. extension CalculatorLogic { - private func handleLastOperatorIfNeeded() { - if isLastInputOperator { - self.expression.removeLast() - } - } - + /// 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() { - if self.expression.count > 1 && self.expression[expression.startIndex] == "0" { - if expression[expression.index(expression.startIndex, offsetBy: 1)].isNumber { - self.expression.removeFirst() - } + 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 && !expression[expression.index(expression.endIndex, offsetBy: -2)].isNumber { + 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 - if input == "0" { - self.isLastInputZero = true - } else { - self.isLastInputZero = false - } } internal func appendOperatorToExpression(_ input: String) { From e0a193c46112bd817ab30f9193639bda83895247 Mon Sep 17 00:00:00 2001 From: Maccha <129703581+DoyleHWorks@users.noreply.github.com> Date: Fri, 22 Nov 2024 01:59:10 +0900 Subject: [PATCH 14/14] =?UTF-8?q?=F0=9F=93=9D=20README:=20Update=20accordi?= =?UTF-8?q?ng=20to=20version=200.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 139 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 82 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 06c177d..a752ee7 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,98 @@ -# CalculatorApp -## Codebase -### Folder Organization -``` +# 📱 CalculatorApp + +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). + + +## 📅 Project Scope + +| 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-Storyboard 0.0.8 (Deprecated) + +Implemented the basic UI and button actions using Storyboard. + +![image](https://github.com/user-attachments/assets/4c6fba3c-4cf3-40c5-983f-ae5cfc953c04) + +## 🛠️ CalculatorApp-Codebase 0.2.0 + +### 📂 Folder Organization +``` CalculatorApp-Codebase/ -│ ├── App/ │ ├── AppDelegate.swift │ ├── SceneDelegate.swift │ ├── Info.plist │ └── LaunchScreen.storyboard -│ ├── Model/ -│ └── CalculatorLogic.swift -│ -├── View/ -│ ├── CalculatorButton.swift -│ └── ExpressionLabel.swift -│ -├── Controller/ +│ ├── CalculatorLogic.swift +│ └── CalculatorLogicDelegate.swift +├── ViewController/ │ └── ViewController.swift -│ └── Resources/ └── Assets.xcassets ``` ---- +### 🖼️ 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)| +|---|---|---| -## Codebase.Ver.0.0.8 -- CalculatorApp-Storyboard Ver.0.0.8 (Lv.8) 파일이 포함됨 -- CalculatorApp-Codebase Ver.0.0.8 (Lv.8) +### 📐 Main Features & Considerations -### CalculatorApp-Storyboard Ver.0.0.8 (Lv.8) -숫자와 연산자 버튼을 누를 때마다 `expression` 문자열에 추가하고, 이를 화면(expressionLabel)에 표시한다. -"=" 버튼을 누르면 수식을 계산하여 결과를 표시하며, "AC" 버튼으로 초기화한다. -곱셈(`×`)과 나눗셈(`÷`)은 실제 계산을 위해 `*`, `/`로 변환된다. +#### 🎨 User Interface -![image](https://github.com/user-attachments/assets/4c6fba3c-4cf3-40c5-983f-ae5cfc953c04) +- **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. + +#### 🏗️ 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. + +#### 🧩 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. -### CalculatorApp-Codebase Ver.0.0.8 (Lv.8) - -#### �️ **열거형을 활용한 버튼 관리 (`CalculatorButton`)** -- `enum CalculatorButton`으로 버튼의 상태를 정의: - - **숫자 버튼**과 **연산자 버튼**을 동일한 열거형으로 관리. - - 각 버튼의 **타이틀, 배경색** 등을 프로퍼티로 제공. - - **`button` 프로퍼티**를 통해 버튼 인스턴스를 생성하여 일관성 있는 설정 제공. - - **객체 지향 원칙 활용**: 중복되는 버튼 설정(프레임, 색상, 텍스트 스타일)을 열거형 내부에서 한 번만 정의하여 중복 코드 최소화. - -#### � **버튼 액션 처리의 통합적 구현** -- **모든 버튼의 액션을 `buttonAction` 메서드로 통합**: - - `UIButton`의 **타이틀 기반으로 동작 결정** (`titleLabel?.text`). - - **숫자와 연산자 구분 없이 동일한 메서드**에서 처리. - - `switch` 문을 활용하여 각 버튼의 기능(숫자 추가, 연산자 추가, 초기화, 계산) 실행. - - **선언형 접근법**: 버튼마다 개별 메서드를 만드는 대신, 액션 메서드를 하나로 통합하여 유지보수성 강화. - -#### �️ **`didSet`를 활용한 표현식 업데이트** -- **`expression` 프로퍼티에 `didSet` 사용**: - - **상태 변화 감지**: `expression`이 변경될 때마다 라벨에 자동으로 반영. - - **숫자 입력 예외 처리**: `0`으로 시작하는 표현식을 자동 정리(두 번째 문자가 숫자일 경우 첫 번째 `0` 제거). - -#### � **동적 레이아웃 설정 (`SnapKit`)** -- **Stack View**: - - 수직 스택(superStack)을 상위 컨테이너로 두고, 내부에 4개의 **수평 스택**을 배치. - - 각 수평 스택에 버튼 4개씩 배치하여 계산기의 숫자 및 연산자 버튼을 균일하게 정렬. -- **SnapKit의 제약 조건 사용**: - - **수평 스택 간 간격** 및 각 버튼의 크기를 동적으로 조정. - - 화면 크기에 따라 적절한 버튼 배치가 자동 조정되도록 설정. - -#### � **`NSExpression`을 활용한 수식 평가** -- 수식 계산은 `NSExpression`을 활용하여 문자열 수식을 평가: - - 입력된 수식을 단순히 텍스트가 아닌 **실제 수식으로 변환**하여 계산. - - **커스텀 기호(`×`, `÷`)를 표준 수학 기호(`*`, `/`)로 변환**하는 전처리 함수(`changeMathSymbols`) 포함. - - `NSExpression`의 내장 계산 기능을 사용하여 **간결하고 안전한 계산 로직** 구현. +## 📦 How to Install +1. Clone this repository: + ```bash + git clone https://github.com/DoyleHWorks/CalculatorApp.git + ```