스파르타 코딩 클럽 - iOS 스타터 6기/본 캠프

25. 스파르타 코딩 클럽 - 계산기 UI 만들어보기 (CodeBase) Lv6 ~ Lv8

seongpil Heo 2025. 4. 3. 19:58

🧑‍💻 코드베이스 UI로 계산기 앱 만들기

입문 강의에서 배운 것들을 복습하며 지금까지 Playground에서 로직만 구현했던 계산기에 UI를 더해, 실제 앱으로 완성해 봅시다.


 🧑‍💻 Lv8까지의 구현 코드 - MVC 패턴 적용

[ MVC - Model ]

//
//  Model.swift
//  CalculatorUI
//
//  Created by 허성필 on 4/1/25.
//

import Foundation

class Model {
    
    /// 수식 문자열을 넣으면 계산해주는 메서드.
    ///
    /// 예를 들어 expression 에 "1+2+3" 이 들어오면 6 을 리턴한다.
    /// 잘못된 형식의 수식을 넣으면 앱이 크래시 난다. ex) "1+2++"
    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
        }
    }
    
}

 

[ MVC - View ]

//
//  View.swift
//  CalculatorUI
//
//  Created by 허성필 on 4/1/25.
//

import UIKit

class View: UIView {
    
    let label = UILabel() // label 생성
    let verticalStackView = UIStackView()
    private var result: String = "0"
    
    let buttonLabels = [["7", "8", "9", "+"],["4", "5", "6", "-"],
                        ["1", "2", "3", "*"], ["AC", "0", "=", "/"]]

    override init(frame: CGRect) {
        super.init(frame: frame)
        makeLabelUI() // 라벨 생성
        makeVerticalStackView() // 1개의 Vertical 스택 뷰를 생성
        for i in buttonLabels { // 4개의 Horizontal 스택 뷰를 생성
            makeHorizontalStackView(i) //
        }
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    // 숫자 라벨 만들기
    private func makeLabelUI() { // 숫자 라벨 만들기
        self.backgroundColor = .black
        label.textColor = .white
        label.text = result // Lv6 기본 텍스트 변경
        label.textAlignment = .right
        label.font = .boldSystemFont(ofSize: 60)
        
        self.addSubview(label)
        
        label.snp.makeConstraints{ make in
            make.leading.equalToSuperview().offset(30)
            make.trailing.equalToSuperview().offset(-30)
            make.top.equalToSuperview().offset(200)
            make.height.equalTo(100)
        }
    }
    
    private func makeVerticalStackView() { // Vertical 스택 뷰를 생성 함수
        verticalStackView.axis = .vertical
        verticalStackView.backgroundColor = .black
        verticalStackView.spacing = 10
        verticalStackView.distribution = .fillEqually
        
        self.addSubview(verticalStackView)
        
        verticalStackView.snp.makeConstraints{ make in
            make.width.equalTo(350)
            make.top.equalTo(label.snp.bottom).offset(60)
            make.centerX.equalToSuperview()
        }
    }
    
    private func makeButton(_ text: String) -> UIButton { // 버튼을 만드는 함수
        let button = UIButton()
        
        button.setTitle(text, for: .normal)
        button.titleLabel?.font = .boldSystemFont(ofSize: 30)
        button.layer.cornerRadius = 40
        button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchDown)
        
        // 해당 text가 들어오면 backgroundColor를 orange색으로 변경
        if ["+", "-", "*", "/", "="].contains(text) {
            button.backgroundColor = .orange
        } else if ["AC"].contains(text) {
            button.backgroundColor = .red
        } else {
            button.backgroundColor = UIColor(red: 58/255, green: 58/255, blue: 58/255, alpha: 1.0)
        }
        
        button.snp.makeConstraints{ make in
            make.width.height.equalTo(80)
        }
        
        return button
    }
    
    // 버튼 클릭 이벤트
    @objc
    private func buttonTapped(_ sender: UIButton) {
        let model = Model()
        if let buttonText = sender.currentTitle {
            if buttonText == "AC" {
                result = "0"
                label.text = result
                label.textColor = .white
            } else if buttonText == "=" { // 함수 사용전에 입력 받아져있는 값을 검증
                if ["+", "-", "*", "/"].contains(result.suffix(1)) {
                    label.text = "Error!"
                    label.textColor = .yellow
                } else {
                    if let result = model.calculate(expression: result) {
                        label.text = "\(result)"
                        label.textColor = .white
                    } else {
                        label.text = "0"
                        label.textColor = .white
                    }
                }
            } else if label.text == "Error!" {
                result = buttonText
                label.text = result
                label.textColor = .white
            } else if label.text == "0" {
                result = buttonText
                label.text = result
                label.textColor = .white
            } else {
                result = (label.text ?? "") + buttonText
                label.text = result
                label.textColor = .white
            }
        }
    }
    
    // Horizontal 스택 뷰를 생성 함수
    private func makeHorizontalStackView(_ buttonLabel: [String]) {
        
        let horizontalStackView = UIStackView()
        
        verticalStackView.addArrangedSubview(horizontalStackView)
        
        horizontalStackView.axis = .horizontal
        horizontalStackView.backgroundColor = .black
        horizontalStackView.spacing = 10
        horizontalStackView.distribution = .fillEqually
        
        horizontalStackView.snp.makeConstraints{ make in
            make.height.equalTo(80)
        }
        
        for i in buttonLabel {
            horizontalStackView.addArrangedSubview(makeButton(i)) // 4번 반복
        }

    }
}

 

[ MVC - ViewController ]

//
//  ViewController.swift
//  CalculatorUI
//
//  Created by 허성필 on 3/28/25.
//

import UIKit
import SnapKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view = View()
    }
    
}

[ 계산기 실행 예 ]

 


 🎯 Trouble Shooting

// 버튼 클릭 이벤트
    @objc
    private func buttonTapped(_ sender: UIButton) {
        let model = Model()
        if let buttonText = sender.currentTitle {
            if buttonText == "AC" {
                result = "0"
                label.text = result
                label.textColor = .white
            } else if buttonText == "=" { // 함수 사용전에 입력 받아져있는 값을 검증
                if ["+", "-", "*", "/"].contains(result.suffix(1)) {
                    label.text = "Error!"
                    label.textColor = .yellow
                } else {
                    if let result = model.calculate(expression: result) {
                        label.text = "\(result)"
                        label.textColor = .white
                    } else {
                        label.text = "0"
                        label.textColor = .white
                    }
                }
            } else if label.text == "Error!" {
                result = buttonText
                label.text = result
                label.textColor = .white
            } else if label.text == "0" {
                result = buttonText
                label.text = result
                label.textColor = .white
            } else {
                result = (label.text ?? "") + buttonText
                label.text = result
                label.textColor = .white
            }
        }
    }

 

처음 버튼 이벤트를 만들 때, 각각의 버튼에 접근? 하는 것이 어려웠다.

버튼을 각각 빼줘야 하는지, 어떻게 접근해야 하는지를 몰라서 애를 먹었다.

 

그러다가 찾은 방법이 buttonTapped() 함수 안에 UIButton을 매개변수로 받아 사용하는 방법이었다.

사용자가 버튼을 누르면 UIButton을 매개변수로 받고, 받은 버튼의 currentTitle을 변수로 저장하여 처리하는 방법이었다.

 

사용자가 누른 버튼이 "AC"일 때, "="일 때, "연산기호"일 때, 숫자일 때로 나누어서 Label.text의 값을 변경하는 식으로 처리했다.

이 방법을 사용함으로써 나는 사용자가 누른 버튼에 대해 접근이 가능했고,

원하는 기능을 완성할 수 있었다. 


 ✓ TIL

이번 계산기 만들기 과제를 통해 코드베이스로 작성하는 UI에 대해서 조금 알아갈 수 있었다.

SnapKit 라이브러리를 이용하여 제약조건을 구성하고 UI를 View에 배치하는 방법도 배울 수 있었다.

다양한 예외처리를 수행하면서 앱을 만들 때, 조금 더 고민하고 작업에 들어가는 방법을 배웠다.

 

수요일까지 Lv8단계를 완료하기가 목표였는데 수요일까지 작업을 완료해서 뿌듯하다.

추가로 간단하게라도 MVC 패턴을 적용해 보면서 파일 분리를 시도해 보았다.

 

다음 주부터는 일주일 동안 팀 프로젝트를 진행하는데

팀원들과 프로젝트를 잘 마무리하면 좋겠다!