diff --git a/Calculate/Calculate/CalculatorModel.swift b/Calculate/Calculate/CalculatorModel.swift new file mode 100644 index 0000000..95e669e --- /dev/null +++ b/Calculate/Calculate/CalculatorModel.swift @@ -0,0 +1,51 @@ +// +// CalculatorModel.swift +// Calculate +// +// Created by mun on 11/21/24. +// +import Foundation + +class CalculatorModel { + + // 계산된 결과 반환 + func calculate(expression: String) -> Int? { + guard isValidExpression(expression) else { return nil } + + let expression = NSExpression(format: expression) + if let result = expression.expressionValue(with: nil, context: nil) as? Int { + return result + } else { + return nil + } + } + + // 수식이 유효한지 확인 + private func isValidExpression(_ text: String) -> Bool { + let textArray = Array(text) + var previousChar: String? + + // 시작 문자가 "-"을 제외한 다른 연산 기호라면 false + if Int(String(textArray.first!)) == nil && textArray.first != "-" { + return false + } + + // 마지막 문자가 연산 기호라면 false + if Int(String(textArray.last!)) == nil { + return false + } + + for char in textArray { + let currentChar = String(char) + + if Int(currentChar) == nil { + // 연산 기호가 연속으로 나오면 false + if currentChar == previousChar { + return false + } + } + } + + return true + } +} diff --git a/Calculate/Calculate/CalculatorView.swift b/Calculate/Calculate/CalculatorView.swift new file mode 100644 index 0000000..bd125e2 --- /dev/null +++ b/Calculate/Calculate/CalculatorView.swift @@ -0,0 +1,117 @@ +// +// CalculatorView.swift +// Calculate +// +// Created by mun on 11/21/24. +// + +import UIKit + +import SnapKit + +class CalculatorView: UIView { + + var label = UILabel() + var verticalStackView = UIStackView() + + let buttonDatas = [["7", "8", "9", "+"], + ["4", "5", "6", "-"], + ["1", "2", "3", "*"], + ["AC", "0", "=", "/"]] + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + configureLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // UI 설정 + private func configureUI() { + self.backgroundColor = .black + [label, verticalStackView].forEach() { + self.addSubview($0) + } + + configureLabel() + configureVerticalStackView() + } + + // label 스타일 설정 + private func configureLabel() { + label.text = "0" + label.textColor = .white + label.textAlignment = .right + label.font = .boldSystemFont(ofSize: 60) + } + + // VerticalStackView 스타일 설정 + private func configureVerticalStackView() { + for i in 0..<4 { + let view = makeHorizontalStackView(i) + verticalStackView.addArrangedSubview(view) + } + + verticalStackView.axis = .vertical + verticalStackView.backgroundColor = .black + verticalStackView.spacing = 10 + verticalStackView.distribution = .fillEqually + } + + // horizontalStackView 생성 + private func makeHorizontalStackView(_ verIndex: Int) -> UIStackView { + let horizontalStackView = UIStackView() + + horizontalStackView.axis = .horizontal + horizontalStackView.backgroundColor = .black + horizontalStackView.spacing = 10 + horizontalStackView.distribution = .fillEqually + + for horIndex in 0..<4 { + let button = makeButton(verIndex: verIndex, horIndex: horIndex) + horizontalStackView.addArrangedSubview(button) + } + + return horizontalStackView + } + + // button 생성 + private func makeButton(verIndex: Int, horIndex: Int) -> UIButton { + let button = UIButton() + let title = buttonDatas[verIndex][horIndex] + let color = Int(title) == nil ? .orange : UIColor(red: 58/255, green: 58/255, blue: 58/255, alpha: 1.0) + + button.setTitle(title, for: .normal) + button.titleLabel?.font = .boldSystemFont(ofSize: 30) + button.backgroundColor = color + button.layer.cornerRadius = 80 / 2 + + button.snp.makeConstraints() { + $0.width.height.equalTo(80) + } + + return button + } + + // button 텍스트 변경 + func setLabelText(_ text: String) { + label.text = text + } + + // 오토 레이아웃 설정 + private func configureLayout() { + label.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(30) + $0.top.equalToSuperview().inset(200) + $0.height.equalTo(100) + } + + verticalStackView.snp.makeConstraints { + $0.top.equalTo(label.snp.bottom).offset(60) + $0.centerX.equalToSuperview() + } + } +} diff --git a/Calculate/Calculate/ViewController.swift b/Calculate/Calculate/ViewController.swift index 66658bb..6e5f7f5 100644 --- a/Calculate/Calculate/ViewController.swift +++ b/Calculate/Calculate/ViewController.swift @@ -9,79 +9,56 @@ import UIKit import SnapKit class ViewController: UIViewController { - - let label = UILabel() - let stackView = UIStackView() - let buttons = [UIButton(), UIButton(), UIButton(), UIButton()] - let buttonDatas = ["7", "8", "9", "+"] - - override func viewDidLoad() { - super.viewDidLoad() - - configureUI() - configureLayout() - } - - // UI 설정 - private func configureUI() { - view.backgroundColor = .black - configureLabel() - configureStackView() - - [label, stackView].forEach{ - view.addSubview($0) - } - } - - // label 스타일 설정 - private func configureLabel() { - label.text = "12345" - label.textColor = .white - label.textAlignment = .right - label.font = .boldSystemFont(ofSize: 60) - } - - private func configureStackView() { - stackView.axis = .horizontal - stackView.backgroundColor = .black - stackView.spacing = 10 - stackView.distribution = .fillEqually - - configureButton() - } - - private func configureButton() { - for i in buttons.indices { - buttons[i].setTitle(buttonDatas[i], for: .normal) - buttons[i].titleLabel?.font = .boldSystemFont(ofSize: 30) - buttons[i].backgroundColor = UIColor(red: 58/255, green: 58/255, blue: 58/255, alpha: 1.0) - stackView.addArrangedSubview(buttons[i]) - } + + var calculatorModel: CalculatorModel! + var calculatorView: CalculatorView! + + override func viewDidLoad() { + super.viewDidLoad() + + calculatorModel = CalculatorModel() + calculatorView = CalculatorView(frame: view.bounds) + + view.addSubview(calculatorView) + + configureActions() + } + + // 버튼에 action 연결 + private func configureActions() { + for stackView in calculatorView.verticalStackView.arrangedSubviews { + let stackView = stackView as! UIStackView + + for button in stackView.arrangedSubviews { + let button = button as! UIButton + + button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + } } - - // 레이아웃 설정 - private func configureLayout() { - label.snp.makeConstraints { - $0.leading.trailing.equalToSuperview().inset(30) - $0.top.equalToSuperview().inset(200) - $0.height.equalTo(100) - } - - stackView.snp.makeConstraints { - $0.height.equalTo(80) - $0.top.equalTo(label.snp.bottom).offset(50) - $0.centerX.equalToSuperview() - } - - for button in buttons { - button.snp.makeConstraints { - $0.height.width.equalTo(80) - } - } - + } + + // 버튼 클릭시 실행 + @objc + func buttonTapped(_ sender: UIButton) { + let buttonTitle = sender.currentTitle! + + if buttonTitle == "AC" { + calculatorView.setLabelText("0") + } else if buttonTitle == "=" { + if let result = calculatorModel.calculate(expression: calculatorView.label.text!) { + calculatorView.setLabelText("\(result)") + } + } else { + if calculatorView.label.text == "0" { + calculatorView.setLabelText(buttonTitle) + } else { + calculatorView.setLabelText(calculatorView.label.text! + buttonTitle) + } } + } } + #Preview("ViewController") { - ViewController() + ViewController() } diff --git a/README.md b/README.md index 9106619..44eb059 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,55 @@ -# 계산기 만들기 과제 (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 - } -} +# iOS 계산기 어플 +![Simulator Screen Recording - iPhone 16 Pro - 2024-11-21 at 13 15 02](https://github.com/user-attachments/assets/cf2f83cd-7c56-4300-9fb0-1d68e934e97e) + +## 목차 +1. [프로젝트 소개](#star-프로젝트-소개) +2. [개발 기간](#calendar-개발기간) +3. [기술스택](#hammer_and_wrench-기술스택) +5. [주요기능](#sparkles-주요기능) +6. [개발 환경](#computer-개발-환경) +7. [설치 및 실행 방법](#inbox_tray-설치-및-실행-방법) +8. [트러블 슈팅](#bug-트러블-슈팅) + + +## :star: 프로젝트 소개 +이 프로젝트는 내일배움캠프 4주차 과제로, iOS 개발 기초를 다지기 위해 제작한 **간단한 계산기 어플**입니다. 사용자가 입력한 값을 바탕으로 기본 연산을 수행합니다. +

+## :calendar: 개발기간 +- 2024.11.14.(목) ~ 2024.11.21(목) +

+## :hammer_and_wrench: 기술스택 + +### :building_construction: 아키텍처 +- MVC + +### :art: UI Framworks +- UIKit +- AutoLayout +

+## :sparkles: 주요기능 +- **기본 연산 기능**: 덧셈, 뺄셈, 곱셈, 나눗셈 수행 +- **입력값 에러 처리**: 잘못된 입력값에 대한 에러 처리 +

+## :computer: 개발 환경 +- **Xcode**: 16.1 +- **iOS Deployment Target**: iOS 18.1 +- **iOS Tested Version**: iOS 18.1 (시뮬레이터 및 실제 기기) +

+## :inbox_tray: 설치 및 실행 방법 +1. 이 저장소를 클론합니다. +```bash +git clone https://github.com/name-mun/sparta-ios-project-w4.git ``` +2. 프로젝트 디렉토리로 이동합니다. +```bash +cd sparta-ios-project-w4 -- **버튼 및 라벨 설정** - - 버튼의 색상, 크기, 모양을 설정하고 라벨에 표시될 수식을 업데이트합니다. - ---- - -## 🎯 목표 - -- **기한**: 11월 22일 (금) 낮 12시까지 제출 -- **제출물**: 개인 과제 결과물을 GitHub에 올리고 링크를 제출합니다. - -## 🔗 참고 링크 -- [Swift 기초 및 iOS 개발 환경 설정](https://developer.apple.com/swift/) -- [Auto Layout 사용 가이드](https://developer.apple.com/documentation/uikit/auto_layout/) - ---- - -이번 과제를 통해 UI와 로직의 통합 구현을 연습하고, 협업을 통한 코드 리뷰와 피드백을 통해 더 나은 코드 품질을 만들어 봅시다. +``` +3. Xcode에서 `sparta-ios-project-w4.xcodeproj` 파일을 엽니다. + +4. Xcode에서 빌드 후 실행합니다. +- 실행 대상에서 **iPhone Simulator** 선택 +- **Cmd + R**로 실행 +

+## :bug: 트러블 슈팅 +👉 [NSExpression format 크래시](https://name-mun.tistory.com/38)