신규버전 계산기앱 만들기

호씨·2024년 11월 19일
1

신규버전 계산기앱


기존에 만들던 계산기가 아닌, 애플순정 앱에 있는 계산기 기능을 만들어봐야지 했는데 상당히 난처한 상황에 빠졌다.

추가된 부분

변경점 정리
1. 버튼색상변경

  • 기존의 버튼 색이 아닌 살짝 다른 색상조정
  1. 버튼배치변경
  • 4-4배열이 아닌 4-5배열로 수정
  1. 신규 버튼들 기능구현
  • +/- 기능구현
  • % 기능구현
    • = 표시를 안누르고 바로 계산에 들어사는 현상 발생, 수정예정
  • 소숫점 입력 및 표시기능 추가
    • 정수형으로 떨어지는 숫자의경우 .0이 붙어서 연산처리되고있음, 수정필요

만들어보는 이유

뭔가 기존에 만들던게 아쉬운부분도 있고, 내가 완전히 이해를 한게 맞는지 확인차 해당 프로그램을 만들어보게 되었다.

현재 막히는부분

  1. Uikit
  • AutoLayout에 대한 이해가 부족한지 각 객체들을 만드는 과정에서 생각보다 시간을 많이 잡아먹었다.
  • 버튼들의 디자인 구현.
    • 현재 작성한 코드를 프리뷰로 돌리면 원형이 아닌 알약형태의 버튼으로 출력되고있다.
    • 좌측 하단의 계산기마크 로고 삽입
  1. 알고리즘? 연산오류?
  • 소숫점 적용후 특정 연산? 출력오류

일단 머리 부딪혀보면서 만들어야지 했지만 생각보다 뎌딘 상황이라 내일중으로는 마무리를 지을까 한다.
튜터님 찬스를 잔뜩 써야지

작성한 코드


import UIKit
import SnapKit

class NewCalculator: UIViewController {
    
    // MARK: - Properties
    /// 계산기에 표시되는 현재 숫자 또는 수식을 저장하는 변수
    private var firstNumber = "0"
    
    /// 새로운 계산 시작 여부를 판단하는 플래그
    private var isNewCalculation = false
    
    // MARK: - UI Components
    /// 계산 결과를 표시하는 레이블
    private let label: UILabel = {
        let label = UILabel()
        label.textColor = .white
        label.font = .systemFont(ofSize: 80, weight: .light)
        label.textAlignment = .right
        label.numberOfLines = 1
        label.adjustsFontSizeToFitWidth = true
        label.minimumScaleFactor = 0.5
        return label
    }()
    
    // 각 줄의 버튼들
    private let resetButton = UIButton()      // AC 버튼
    private let plusMinusButton = UIButton()  // +/- 버튼
    private let percentButton = UIButton()    // % 버튼
    private let dividButton = UIButton()      // ÷ 버튼
    
    private let sevenButton = UIButton()      // 7 버튼
    private let eightButton = UIButton()      // 8 버튼
    private let nineButton = UIButton()       // 9 버튼
    private let multplyButton = UIButton()    // × 버튼
    
    private let fourButton = UIButton()       // 4 버튼
    private let fiveButton = UIButton()       // 5 버튼
    private let sixButton = UIButton()        // 6 버튼
    private let minusButton = UIButton()      // − 버튼
    
    private let oneButton = UIButton()        // 1 버튼
    private let twoButton = UIButton()        // 2 버튼
    private let threeButton = UIButton()      // 3 버튼
    private let plusButton = UIButton()       // + 버튼
    
    private let zeroButton = UIButton()       // 첫 번째 0 버튼
    private let zeroButton2 = UIButton()      // 두 번째 0 버튼
    private let dotButton = UIButton()        // . 버튼
    private let equalButton = UIButton()      // = 버튼
    
    // 스택뷰
    private let stackView = UIStackView()     // AC, +/-, %, ÷
    private let stackView1 = UIStackView()    // 7, 8, 9, ×
    private let stackView2 = UIStackView()    // 4, 5, 6, −
    private let stackView3 = UIStackView()    // 1, 2, 3, +
    private let stackView4 = UIStackView()    // 0, 0, ., =
    
    // 수직 스택뷰
    private let verticalStackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 10
        stackView.distribution = .fillEqually
        return stackView
    }()
    

    // MARK: - Lifecycle Methods
    override func viewDidLoad() {
        super.viewDidLoad()
        calculatorUI()
    }
    
    // MARK: - UI Setup Methods
    private func calculatorUI() {
        view.backgroundColor = .black
        setupButtons()
        setupStackViews()
        setupConstraints()
        layoutButtons()
    }
    
    private func setupButton(_ button: UIButton, title: String) {
        button.setTitle(title, for: .normal)
        button.backgroundColor = UIColor(white: 0.23, alpha: 1.0)
        button.setTitleColor(.white, for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.titleLabel?.font = .systemFont(ofSize: 30) //   - 폰트 크기: 30pt
        button.layer.cornerRadius = 40 //   - 모서리: 40pt 라운드 처리
    }
    
    private func colorButton(_ button: UIButton, title: String, color: UIColor) {
        button.setTitle(title, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 34)
        button.backgroundColor = color
        button.setTitleColor(.white, for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.heightAnchor.constraint(equalTo: button.widthAnchor).isActive = true
    }
    
    private func layoutButtons() {
        let screenWidth = UIScreen.main.bounds.width
        let padding: CGFloat = 20
        let spacing: CGFloat = 12
        let availableWidth = screenWidth - (padding * 2) - (spacing * 3)
        let buttonDiameter = availableWidth / 4  // 버튼의 지름
        
        [resetButton, plusMinusButton, percentButton, dividButton,
         sevenButton, eightButton, nineButton, multplyButton,
         fourButton, fiveButton, sixButton, minusButton,
         oneButton, twoButton, threeButton, plusButton,
         zeroButton, zeroButton2, dotButton, equalButton].forEach { button in
            // 가로 세로가 같은 정사각형으로 만들고 지름의 절반으로 cornerRadius 설정
            button.widthAnchor.constraint(equalToConstant: buttonDiameter).isActive = true
            button.heightAnchor.constraint(equalToConstant: buttonDiameter).isActive = true
            button.layer.cornerRadius = buttonDiameter / 2
            button.clipsToBounds = true
        }
    }
    
    private func setupButtons() {
        // 숫자 버튼 설정
        setupButton(zeroButton, title: "0")
        setupButton(zeroButton2, title: "0")
        setupButton(oneButton, title: "1")
        setupButton(twoButton, title: "2")
        setupButton(threeButton, title: "3")
        setupButton(fourButton, title: "4")
        setupButton(fiveButton, title: "5")
        setupButton(sixButton, title: "6")
        setupButton(sevenButton, title: "7")
        setupButton(eightButton, title: "8")
        setupButton(nineButton, title: "9")
        setupButton(dotButton, title: ".")
        
        // 연산자 버튼 설정
        colorButton(resetButton, title: "AC", color: .lightGray)
        colorButton(plusMinusButton, title: "+/-", color: .lightGray)
        colorButton(percentButton, title: "%", color: .lightGray)
        colorButton(dividButton, title: "÷", color: .systemOrange)
        colorButton(multplyButton, title: "×", color: .systemOrange)
        colorButton(minusButton, title: "−", color: .systemOrange)
        colorButton(plusButton, title: "+", color: .systemOrange)
        colorButton(equalButton, title: "=", color: .systemOrange)
        
        // 버튼 액션 설정
        setupButtonActions()
    }
    
    private func setupStackViews() {
        // 각 가로 스택뷰 설정
        [stackView, stackView1, stackView2, stackView3, stackView4].forEach {
            $0.axis = .horizontal
            $0.spacing = 10
            $0.distribution = .fillEqually
        }
        
        // 첫번째 줄 (AC, +/-, %, ÷)
        stackView.addArrangedSubview(resetButton)
        stackView.addArrangedSubview(plusMinusButton)
        stackView.addArrangedSubview(percentButton)
        stackView.addArrangedSubview(dividButton)
        
        // 두번째 줄 (7, 8, 9, ×)
        stackView1.addArrangedSubview(sevenButton)
        stackView1.addArrangedSubview(eightButton)
        stackView1.addArrangedSubview(nineButton)
        stackView1.addArrangedSubview(multplyButton)
        
        // 세번째 줄 (4, 5, 6, -)
        stackView2.addArrangedSubview(fourButton)
        stackView2.addArrangedSubview(fiveButton)
        stackView2.addArrangedSubview(sixButton)
        stackView2.addArrangedSubview(minusButton)
        
        // 네번째 줄 (1, 2, 3, +)
        stackView3.addArrangedSubview(oneButton)
        stackView3.addArrangedSubview(twoButton)
        stackView3.addArrangedSubview(threeButton)
        stackView3.addArrangedSubview(plusButton)
        
        // 다섯번째 줄 (0, 0, ., =)
        stackView4.addArrangedSubview(zeroButton)
        stackView4.addArrangedSubview(zeroButton2)
        stackView4.addArrangedSubview(dotButton)
        stackView4.addArrangedSubview(equalButton)
        
        // 수직 스택뷰에 추가
        verticalStackView.addArrangedSubview(stackView)
        verticalStackView.addArrangedSubview(stackView1)
        verticalStackView.addArrangedSubview(stackView2)
        verticalStackView.addArrangedSubview(stackView3)
        verticalStackView.addArrangedSubview(stackView4)
        
        // 메인 뷰에 추가
        view.addSubview(label)
        view.addSubview(verticalStackView)
    }
    
    private func setupConstraints() {
        label.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide).offset(20)
            $0.leading.equalToSuperview().offset(20)
            $0.trailing.equalToSuperview().offset(-20)
            $0.height.equalTo(120)
        }
        
        verticalStackView.snp.makeConstraints {
            $0.top.equalTo(label.snp.bottom).offset(20)
            $0.leading.equalToSuperview().offset(20)
            $0.trailing.equalToSuperview().offset(-20)
            $0.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20)
        }
    }
    
    private func setupButtonActions() {
        // 숫자 버튼 액션
        oneButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        twoButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        threeButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        fourButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        fiveButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        sixButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        sevenButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        eightButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        nineButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        zeroButton.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        zeroButton2.addTarget(self, action: #selector(numberButtonTapped(_:)), for: .touchUpInside)
        dotButton.addTarget(self, action: #selector(dotButtonTapped), for: .touchUpInside)
        
        // 연산자 버튼 액션
        resetButton.addTarget(self, action: #selector(resetButtonTapped), for: .touchUpInside)
        plusMinusButton.addTarget(self, action: #selector(plusMinusButtonTapped), for: .touchUpInside)
        percentButton.addTarget(self, action: #selector(percentButtonTapped), for: .touchUpInside)
        plusButton.addTarget(self, action: #selector(operatorButtonTapped(_:)), for: .touchUpInside)
        minusButton.addTarget(self, action: #selector(operatorButtonTapped(_:)), for: .touchUpInside)
        multplyButton.addTarget(self, action: #selector(operatorButtonTapped(_:)), for: .touchUpInside)
        dividButton.addTarget(self, action: #selector(operatorButtonTapped(_:)), for: .touchUpInside)
        equalButton.addTarget(self, action: #selector(equalButtonTapped), for: .touchUpInside)
    }
    
    // MARK: - Button Actions
    @objc private func numberButtonTapped(_ sender: UIButton) {
        guard let number = sender.titleLabel?.text else { return }
        if firstNumber == "0" || isNewCalculation {
            firstNumber = number
            isNewCalculation = false
        } else {
            firstNumber += number
        }
        label.text = firstNumber
    }
    
    @objc private func operatorButtonTapped(_ sender: UIButton) {
        guard let op = sender.titleLabel?.text else { return }
        if isLastCharacterOperator() {
            label.text = "error"
        } else {
            firstNumber += op
            label.text = firstNumber
            isNewCalculation = false
        }
    }
    
    @objc private func equalButtonTapped() {
        if firstNumber == "0" {
            label.text = "error"
        } else if isLastCharacterOperator() {
            label.text = "error"
        } else {
            if let result = calculate(expression: firstNumber) {
                label.text = "\(result)"
                firstNumber = "\(result)"
                isNewCalculation = true
            } else {
                label.text = "error"
            }
        }
    }
    
    @objc private func resetButtonTapped() {
        firstNumber = "0"
        isNewCalculation = false
        label.text = firstNumber
    }
    
    @objc private func plusMinusButtonTapped() {
        if let number = Double(firstNumber) {
            firstNumber = "\(-number)"
            label.text = firstNumber
        }
    }
    
    @objc private func percentButtonTapped() {
        if let number = Double(firstNumber) {
            firstNumber = "\(number / 100)"
            label.text = firstNumber
        }
    }
    
    @objc private func dotButtonTapped() {
        if !firstNumber.contains(".") {
            firstNumber += "."
            label.text = firstNumber
        }
    }
    
    // MARK: - Helper Methods
    private func isLastCharacterOperator() -> Bool {
        let operators = ["+", "−", "×", "÷"]
        guard let lastChar = firstNumber.last.map(String.init) else { return false }
        return operators.contains(lastChar)
    }
    
    private func calculate(expression: String) -> Double? {
        // 연산자 변환 (iOS 스타일 → 계산 가능한 형태)
        var expr = expression
        expr = expr.replacingOccurrences(of: "×", with: "*")
        expr = expr.replacingOccurrences(of: "÷", with: "/")
        expr = expr.replacingOccurrences(of: "−", with: "-")
        
        let mathExpression = NSExpression(format: expr)
        return mathExpression.expressionValue(with: nil, context: nil) as? Double
    }
}

// MARK: - Preview Provider
#Preview {
    NewCalculator()
}

일단 코드, 파일분리는 안하고 단순 기능구현에만 집중한 파일인지라 모든 기능들을 원하는대로 수정한 다음에 디자인 패턴에 맞게 수정을 해볼 예정이다.
스스로 불러온 재앙에 짖눌려, 그런짓은 하지 말아야 했는데

일단 이왕 시작했으니 죽이되던 밥이되던 마무리는 지어야할것같으니 목요일 전까지 끝내는것이 목적이다.

profile
이것저것 많이 해보고싶은 사람

0개의 댓글