Ch 3. 앱 개발 입문 주차 과제
Lv8. 내가 작성한 코드
class ViewController: UIViewController {
var number = "0"
let label = UILabel()
let button = UIButton()
private var titles = [["7", "8", "9", "+"], ["4", "5", "6", "-"], ["1", "2", "3", "*"], ["AC", "0", "=", "/"]]
private var verticalStackView = UIStackView()
private var stackView1 = UIStackView()
private var stackView2 = UIStackView()
private var stackView3 = UIStackView()
private var stackView4 = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
setLabel()
setUI()
}
private func setLabel() {
view.backgroundColor = .black
label.text = "\(number)"
label.textAlignment = .right
label.textColor = .white
label.font = UIFont.boldSystemFont(ofSize: 60)
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30),
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 200),
label.heightAnchor.constraint(equalToConstant: 100)
])
}
func setUI() {
let setButton1 = setButton(titles[0], #selector(buttonTapped))
let setButton2 = setButton(titles[1], #selector(buttonTapped))
let setButton3 = setButton(titles[2], #selector(buttonTapped))
let setButton4 = setButton(titles[3], #selector(buttonTapped))
stackView1 = makeHorizontalStackView(setButton1)
stackView2 = makeHorizontalStackView(setButton2)
stackView3 = makeHorizontalStackView(setButton3)
stackView4 = makeHorizontalStackView(setButton4)
let arrStackView = fourStackView()
verticalStackView = makeVerticalStackView(arrStackView)
}
// 타이틀이 바뀌어 적용되는 버튼을 만들고 배열로 묶어주는 함수
private func setButton(_ titles : [String], _ action: Selector) -> [UIButton] {
var arrButtons: [UIButton] = []
let operate = ["+", "-", "*", "AC", "=", "/"]
for title in titles {
let button = UIButton()
button.addTarget(self, action: action, for: .touchDown)
button.setTitle(title, for: .normal)
// 만약에 title이 operate의 요소를 포함하고 있지 않으면
if !operate.contains(title) {
button.backgroundColor = UIColor(red: 58/255, green: 58/255, blue: 58/255, alpha: 1.0)
} else {
button.backgroundColor = UIColor.orange
}
button.titleLabel?.font = .boldSystemFont(ofSize: 30)
button.layer.cornerRadius = 40
arrButtons.append(button)
}
return arrButtons
}
// 4개의 버튼 배열을 묶어서 1개의 스택뷰로 만들어주는 함수
private func makeHorizontalStackView(_ views: [UIButton]) -> UIStackView {
let horizontalStackView = UIStackView()
horizontalStackView.axis = .horizontal
horizontalStackView.backgroundColor = .black
horizontalStackView.spacing = 10
horizontalStackView.distribution = .fillEqually
horizontalStackView.addArrangedSubview(views[0])
horizontalStackView.addArrangedSubview(views[1])
horizontalStackView.addArrangedSubview(views[2])
horizontalStackView.addArrangedSubview(views[3])
view.addSubview(horizontalStackView)
horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
horizontalStackView.heightAnchor.constraint(equalToConstant: 80)
])
return horizontalStackView
}
// 4개의 스택뷰를 묶어서 배열로 리턴하는 함수
private func fourStackView() -> [UIStackView] {
let arrStackView = [stackView1, stackView2, stackView3, stackView4]
return arrStackView
}
// 스택뷰 배열을 받아서 UIStackView로 리턴하는 함수 (여기서 Vertical StackView로 전환)
private func makeVerticalStackView(_ stackViews: [UIStackView]) -> UIStackView {
verticalStackView.axis = .vertical
verticalStackView.backgroundColor = .black
verticalStackView.spacing = 10
verticalStackView.distribution = .fillEqually
verticalStackView.addArrangedSubview(stackViews[0])
verticalStackView.addArrangedSubview(stackViews[1])
verticalStackView.addArrangedSubview(stackViews[2])
verticalStackView.addArrangedSubview(stackViews[3])
view.addSubview(verticalStackView)
verticalStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
verticalStackView.widthAnchor.constraint(equalToConstant: 350),
verticalStackView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 60),
verticalStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
return verticalStackView
}
@objc
private func buttonTapped(_ sender: UIButton) {
guard let title = sender.currentTitle else { return }
switch title {
case "=":
if let result = calculate(expression: number) {
number = "\(result)"
label.text = "\(number)"
}
case "AC":
number = "0"
label.text = "\(number)"
default:
number += title
if number.first == "0" {
number.removeFirst()
}
label.text = "\(number)"
}
}
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
}
}
}
}
위 계산기는 연산자를 2번 입력하면 2번 입력되는 문제가 있다.
따라서 이를 수정해야하는데,
이 작업을 하기 전에 MVC 패턴에 대해 학습을 했기 때문에
MVC 패턴으로 역할과 책임을 분리하고자 한다.
⎮ MVC 패턴 적용하기
MVC 패턴이 뭔데?
Swift | MVC 패턴이 뭔데?
MVC 패턴(Model, View, Controller) 객체지향 프로그래밍 안에서는 서로 메시지를 주고 받으며 일처리를 한다.객체를 너무 중구난방으로 만들어놓으면 객체끼리 충돌이 일어나는 현상이 있을 수 있다.
uddt.tistory.com
이스승을 통해 MVC 패턴을 공부하고,
작업한 코드를 모델, 뷰, 컨트롤러로 구분하고자 했다.
(물론 프로젝트 특성상 완벽하게 분리가 되는 것은 아니다.)
일단, Model에서 데이터를 받아주려면 Delegate 패턴까지 사용해야해서 바로 구현하기에는 난이도가 있었다.
먼저 View랑 ViewController를 분리하는 연습을 해보고,
그 뒤 Model까지 분리해보자.
⎮ View / ViewController 분리하기
먼저 View와 ViewController를 분리하기 위해서,
폴더와 Swift 파일을 만들어 구분해주었다.
만들어진 View에 아래의 코드를 추가하지 않으면,
View의 UI를 초기화해주지 못한다.
init(frame:), init(coder:) 이해하기
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
이제 ViewController에 들어있던 데이터 중 View 관련된 데이터를 옮겨주면 된다.
데이터를 다 옮기고나면 다음과 같은 에러창을 만날 수 있다.
이 에러는 보통 view. 으로 시작하는 부분에서 발생하는데,
이유는 ViewController가 가지고 있던 view에 접근할 수 없기 때문이다.
우리가 분리해서 만든 것이 무엇인가? 바로 View다.
따라서 우리는 view.을 self.으로 바꿔주면 된다.
변경한 코드는 아래와 같다.
- 분리한 코드
// ViewController.swift
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
// OnlyView.swift
import UIKit
class OnlyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setLabel()
setUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
var number = "0"
let label = UILabel()
let button = UIButton()
private var titles = [["7", "8", "9", "+"], ["4", "5", "6", "-"], ["1", "2", "3", "*"], ["AC", "0", "=", "/"]]
private var verticalStackView = UIStackView()
private var stackView1 = UIStackView()
private var stackView2 = UIStackView()
private var stackView3 = UIStackView()
private var stackView4 = UIStackView()
private func setLabel() {
self.backgroundColor = .black
label.text = "\(number)"
label.textAlignment = .right
label.textColor = .white
label.font = UIFont.boldSystemFont(ofSize: 60)
self.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -30),
label.topAnchor.constraint(equalTo: self.topAnchor, constant: 200),
label.heightAnchor.constraint(equalToConstant: 100)
])
}
func setUI() {
let setButton1 = setButton(titles[0], #selector(buttonTapped))
let setButton2 = setButton(titles[1], #selector(buttonTapped))
let setButton3 = setButton(titles[2], #selector(buttonTapped))
let setButton4 = setButton(titles[3], #selector(buttonTapped))
stackView1 = makeHorizontalStackView(setButton1)
stackView2 = makeHorizontalStackView(setButton2)
stackView3 = makeHorizontalStackView(setButton3)
stackView4 = makeHorizontalStackView(setButton4)
let arrStackView = fourStackView()
verticalStackView = makeVerticalStackView(arrStackView)
}
// 타이틀이 바뀌어 적용되는 버튼을 만들고 배열로 묶어주는 함수
private func setButton(_ titles : [String], _ action: Selector) -> [UIButton] {
var arrButtons: [UIButton] = []
let operate = ["+", "-", "*", "AC", "=", "/"]
for title in titles {
let button = UIButton()
button.addTarget(self, action: action, for: .touchDown)
button.setTitle(title, for: .normal)
// 만약에 title이 operate의 요소를 포함하고 있지 않으면
if !operate.contains(title) {
button.backgroundColor = UIColor(red: 58/255, green: 58/255, blue: 58/255, alpha: 1.0)
} else {
button.backgroundColor = UIColor.orange
}
button.titleLabel?.font = .boldSystemFont(ofSize: 30)
button.layer.cornerRadius = 40
arrButtons.append(button)
}
return arrButtons
}
// 4개의 버튼 배열을 묶어서 1개의 스택뷰로 만들어주는 함수
private func makeHorizontalStackView(_ views: [UIButton]) -> UIStackView {
let horizontalStackView = UIStackView()
horizontalStackView.axis = .horizontal
horizontalStackView.backgroundColor = .black
horizontalStackView.spacing = 10
horizontalStackView.distribution = .fillEqually
horizontalStackView.addArrangedSubview(views[0])
horizontalStackView.addArrangedSubview(views[1])
horizontalStackView.addArrangedSubview(views[2])
horizontalStackView.addArrangedSubview(views[3])
self.addSubview(horizontalStackView)
horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
horizontalStackView.heightAnchor.constraint(equalToConstant: 80)
])
return horizontalStackView
}
// 4개의 스택뷰를 묶어서 배열로 리턴하는 함수
private func fourStackView() -> [UIStackView] {
let arrStackView = [stackView1, stackView2, stackView3, stackView4]
return arrStackView
}
// 스택뷰 배열을 받아서 UIStackView로 리턴하는 함수 (여기서 Vertical StackView로 전환)
private func makeVerticalStackView(_ stackViews: [UIStackView]) -> UIStackView {
verticalStackView.axis = .vertical
verticalStackView.backgroundColor = .black
verticalStackView.spacing = 10
verticalStackView.distribution = .fillEqually
verticalStackView.addArrangedSubview(stackViews[0])
verticalStackView.addArrangedSubview(stackViews[1])
verticalStackView.addArrangedSubview(stackViews[2])
verticalStackView.addArrangedSubview(stackViews[3])
self.addSubview(verticalStackView)
verticalStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
verticalStackView.widthAnchor.constraint(equalToConstant: 350),
verticalStackView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 60),
verticalStackView.centerXAnchor.constraint(equalTo: self.centerXAnchor)
])
return verticalStackView
}
@objc
private func buttonTapped(_ sender: UIButton) {
guard let title = sender.currentTitle else { return }
switch title {
case "=":
if let result = calculate(expression: number) {
number = "\(result)"
label.text = "\(number)"
}
case "AC":
number = "0"
label.text = "\(number)"
default:
number += title
if number.first == "0" {
number.removeFirst()
}
label.text = "\(number)"
}
}
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
}
}
}
이제 정상적으로 build가 된다.
빌드 결과는 다음과 같다.
분명 큰 수정 없이 코드를 분리해줬을 뿐인데, 화면에 표시되지 않는다.
왜 그럴까?
⎮ View / ViewController 분리하기 : 트러블 슈팅
위 문제가 발생한 이유는 다음과 같다.
View 자체에는 UI 구현이 다 되어있지만, ViewController에 연결하지 않은 상태이다.
즉, ViewController가 어떤 View를 띄울지 지정되어 있지 않았다.
View 지정을 해주면 쉽게 해결된다.
이를 위해 ViewController에서 코드를 수정해주었다.
//ViewController.swift
import UIKit
class ViewController: UIViewController {
let onlyView = OnlyView()
override func viewDidLoad() {
super.viewDidLoad()
self.view = onlyView
}
}
⎮ Model / View / ViewController 분리하기
가장 어려운 시간이다.
이제 View에 담아두었던 Data들을 Model로 분리해야한다.
지금이 가장 오류가 많이 나는 순간이므로, 커밋을 잘 해두어야 한다.
Model과 View를 분리하기 위해서,
폴더와 Swift 파일을 만들어 구분해주었다.
View와 ViewController를 구분할 때
편의상 View 안에 계산하는 메서드와 버튼 클릭시 동작하는 메서드를 다 모아두었지만
사실 연습을 위해 세팅된 것이지, 핏하게 분리된 것이 아니다.
이제 MVC 패턴 구현을 위해 데이터를 따로 빼주어야한다.
NSExpression으로 계산을 실행해주는 메서드는 DataModel에 담아주었다
- 분리한 코드
// DataModel.swift
import Foundation
class DataModel {
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
}
}
}
// ViewController.swift
import UIKit
class ViewController: UIViewController {
let onlyView = OnlyView()
override func viewDidLoad() {
super.viewDidLoad()
self.view = onlyView
}
}
// OnlyView.swift
import UIKit
class OnlyView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setLabel()
setUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
var number = "0"
let label = UILabel()
let button = UIButton()
private var titles = [["7", "8", "9", "+"], ["4", "5", "6", "-"], ["1", "2", "3", "*"], ["AC", "0", "=", "/"]]
private var verticalStackView = UIStackView()
private var stackView1 = UIStackView()
private var stackView2 = UIStackView()
private var stackView3 = UIStackView()
private var stackView4 = UIStackView()
private func setLabel() {
self.backgroundColor = .black
label.text = "\(number)"
label.textAlignment = .right
label.textColor = .white
label.font = UIFont.boldSystemFont(ofSize: 60)
self.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -30),
label.topAnchor.constraint(equalTo: self.topAnchor, constant: 200),
label.heightAnchor.constraint(equalToConstant: 100)
])
}
func setUI() {
let setButton1 = setButton(titles[0], #selector(buttonTapped))
let setButton2 = setButton(titles[1], #selector(buttonTapped))
let setButton3 = setButton(titles[2], #selector(buttonTapped))
let setButton4 = setButton(titles[3], #selector(buttonTapped))
stackView1 = makeHorizontalStackView(setButton1)
stackView2 = makeHorizontalStackView(setButton2)
stackView3 = makeHorizontalStackView(setButton3)
stackView4 = makeHorizontalStackView(setButton4)
let arrStackView = fourStackView()
verticalStackView = makeVerticalStackView(arrStackView)
}
// 타이틀이 바뀌어 적용되는 버튼을 만들고 배열로 묶어주는 함수
private func setButton(_ titles : [String], _ action: Selector) -> [UIButton] {
var arrButtons: [UIButton] = []
let operate = ["+", "-", "*", "AC", "=", "/"]
for title in titles {
let button = UIButton()
button.addTarget(self, action: action, for: .touchDown)
button.setTitle(title, for: .normal)
// 만약에 title이 operate의 요소를 포함하고 있지 않으면
if !operate.contains(title) {
button.backgroundColor = UIColor(red: 58/255, green: 58/255, blue: 58/255, alpha: 1.0)
} else {
button.backgroundColor = UIColor.orange
}
button.titleLabel?.font = .boldSystemFont(ofSize: 30)
button.layer.cornerRadius = 40
arrButtons.append(button)
}
return arrButtons
}
// 4개의 버튼 배열을 묶어서 1개의 스택뷰로 만들어주는 함수
private func makeHorizontalStackView(_ views: [UIButton]) -> UIStackView {
let horizontalStackView = UIStackView()
horizontalStackView.axis = .horizontal
horizontalStackView.backgroundColor = .black
horizontalStackView.spacing = 10
horizontalStackView.distribution = .fillEqually
horizontalStackView.addArrangedSubview(views[0])
horizontalStackView.addArrangedSubview(views[1])
horizontalStackView.addArrangedSubview(views[2])
horizontalStackView.addArrangedSubview(views[3])
self.addSubview(horizontalStackView)
horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
horizontalStackView.heightAnchor.constraint(equalToConstant: 80)
])
return horizontalStackView
}
// 4개의 스택뷰를 묶어서 배열로 리턴하는 함수
private func fourStackView() -> [UIStackView] {
let arrStackView = [stackView1, stackView2, stackView3, stackView4]
return arrStackView
}
// 스택뷰 배열을 받아서 UIStackView로 리턴하는 함수 (여기서 Vertical StackView로 전환)
private func makeVerticalStackView(_ stackViews: [UIStackView]) -> UIStackView {
verticalStackView.axis = .vertical
verticalStackView.backgroundColor = .black
verticalStackView.spacing = 10
verticalStackView.distribution = .fillEqually
verticalStackView.addArrangedSubview(stackViews[0])
verticalStackView.addArrangedSubview(stackViews[1])
verticalStackView.addArrangedSubview(stackViews[2])
verticalStackView.addArrangedSubview(stackViews[3])
self.addSubview(verticalStackView)
verticalStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
verticalStackView.widthAnchor.constraint(equalToConstant: 350),
verticalStackView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 60),
verticalStackView.centerXAnchor.constraint(equalTo: self.centerXAnchor)
])
return verticalStackView
}
@objc
private func buttonTapped(_ sender: UIButton) {
guard let title = sender.currentTitle else { return }
switch title {
case "=":
if let result = calculate(expression: number) {
number = "\(result)"
label.text = "\(number)"
}
case "AC":
number = "0"
label.text = "\(number)"
default:
number += title
if number.first == "0" {
number.removeFirst()
}
label.text = "\(number)"
}
}
}
핏하게 분리된 것은 아니지만,
MVC 패턴을 연습했다는 것에 의의를 두며...
'스파르타코딩 클럽 > 개인과제' 카테고리의 다른 글
20. 스파르타 코딩클럽 [본캠프 - 계산기 앱 : 정수기(10)] (0) | 2025.04.03 |
---|---|
19. 스파르타 코딩클럽 [본캠프 - 계산기 앱 : 정수기(9)] (0) | 2025.04.02 |
17. 스파르타 코딩클럽 [본캠프 - 계산기 앱 : 정수기(7)] (0) | 2025.04.01 |
16. 스파르타 코딩클럽 [본캠프 - 계산기 앱 : 정수기(6)] (0) | 2025.04.01 |
15. 스파르타 코딩클럽 [본캠프 - 계산기 앱 : 정수기(5)] (0) | 2025.03.31 |