iOS

1. 계산기 코드로 보는 MVC 패턴

728x90

계산기 

1. 처음 시작 코드

- 기본적인 View와 이벤트를 연결한다.

//
//  ViewController.swift
//  calculatorMVC
//
//  Created by Taehoon Kim on 2022/02/01.
//

import UIKit

class ViewController: UIViewController {
    
    // Properties
    
    // 계산기의 계산결과를 보여줄 label 뷰
    let display: UILabel = {
        let label = UILabel()
        label.backgroundColor = .systemBlue
        label.textColor = .white
        label.textAlignment = .right
        label.font = UIFont.systemFont(ofSize: 22)
        return label
    }()
    
    var userIsIntheMiddleOfTyping = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        createUIView()
    }
    
    /// displaydhk keypad 버튼을 stackView로 생성하는 함수
    func createUIView() {
        let keypadStructure = [["*", "/", "+", "-"],
                           ["𝝿", "7", "8", "9"],
                           ["√", "4", "5", "6"],
                           ["save", "1", "2", "3"],
                           ["restore", ".", "0", "="]]
        
        /// 전체적인 스택뷰
        let fullOfCalculatorView = UIStackView(arrangedSubviews: [display])
        fullOfCalculatorView.axis = .vertical
        fullOfCalculatorView.distribution = .fillEqually
        fullOfCalculatorView.spacing = 8
        
        keypadStructure.forEach { (keypadNames) in
            /// 계산기의 한줄을 맡는 Stackview
            let oneOfRowView = UIStackView()
            oneOfRowView.axis = .horizontal
            oneOfRowView.spacing = 8
            oneOfRowView.distribution = .fillEqually
            
            keypadNames.forEach { (keypadName) in
                let button = UIButton()
                button.setTitle(keypadName, for: .normal)
                button.backgroundColor = .systemGray6
                button.setTitleColor(.systemBlue, for: .normal)
                if (keypadName == "𝝿" || keypadName == "√" || keypadName == "cos" || keypadName == "*" || keypadName == "=" ||  keypadName == "+" ||  keypadName == "-" ||  keypadName == "/") { button.addTarget(self, action: #selector(performOperator(_:)), for: .touchUpInside) }
                else { button.addTarget(self, action: #selector(keypadButtonTapped(_:)), for: .touchUpInside) }
                
                oneOfRowView.addArrangedSubview(button)
            }
            fullOfCalculatorView.addArrangedSubview(oneOfRowView)
        }
        
        view.addSubview(fullOfCalculatorView)
        fullOfCalculatorView.translatesAutoresizingMaskIntoConstraints = false
        fullOfCalculatorView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true
        fullOfCalculatorView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8).isActive = true
        fullOfCalculatorView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8).isActive = true
        fullOfCalculatorView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8).isActive = true
    }
    
    // Selector
    
    /// keypad 클릭시 동작하는 함수
    @objc func keypadButtonTapped(_ sender: UIButton) {
        let digit = sender.currentTitle!
        
        if userIsIntheMiddleOfTyping {
            let textCurrentlyInDisplay = display.text!
            display.text = textCurrentlyInDisplay + digit
        } else {
            display.text = digit
        }
        
        userIsIntheMiddleOfTyping = true
    }
    
    @objc func performOperator(_ sender: UIButton) {
        // false로 만들어서 숫자입력시 새롭게 입력받음
        userIsIntheMiddleOfTyping = false
        if let mathematicalSymbol = sender.currentTitle {
            if mathematicalSymbol == "𝝿" {
                display.text = String(M_PI)
            }
        }
    }
}

 

2.  Set, Get 연산프로퍼티로 display text 변경

- 계산 결과를 보여주는 display UILabel의 text값은 String으로 설정해야한다.

- 계산 시에는 Double로 계산해야한다.

=> displayValue로 연산프로퍼티를 만들어서 코드를 간결하게 하자

/// 얻어 올땐 Double로 설정 할땐 String으로 넣어주는 get, set을 작성함
    /// 연산되는 변수를 작성함
    var displayValue: Double {
        get {
            return Double(display.text!)!
        }
        set {
            display.text = String(newValue)
        }
    }
    
    @objc func performOperator(_ sender: UIButton) {
        // false로 만들어서 숫자입력시 새롭게 입력받음
        userIsIntheMiddleOfTyping = false
        if let mathematicalSymbol = sender.currentTitle {
            if mathematicalSymbol == "𝝿" {
                // 이코드를 바꿀 수 있음
                displayValue = .pi
            } else if mathematicalSymbol == "√" {
                displayValue = sqrt(displayValue)
            }
        }
    }

 

3.  Model을 만들자.

performOperator 부분은 앱의 기능이기 때문에 Model에 작성해준다.

+ CalculatorBrain.swift 파일을 추가함

//
//  CaculatorBrain.swift
//  calculatorMVC
//
//  Created by Taehoon Kim on 2022/02/01.
//

import Foundation

class CalculatorBrain
{
    /// accumulator(누산기): 계산기에서 누적되는 값
    private var accumulator = 0.0
    
    /// operand: 연산의 대상값
    func setOperand(_ operand: Double) {
        accumulator = operand
    }
    
    /// symbol에 따라 연산을 수행하는 함수
    func perforOperation(symbol: String) {
        switch symbol {
        case "𝝿": accumulator = .pi
        case "√": accumulator = sqrt(accumulator)
            
        default: break
        }
    }
    
    /// 사용하는 controller에게 accumulator 값을 전달하는 get함수
    var result: Double {
        get {
            return accumulator
        }
    }
    
}

- ViewController 부분 수정

/// 생성한 CalculatorBrain Model 선언
private var brain = CalculatorBrain()
    
    @objc private func performOperator(_ sender: UIButton) {
        // 사용자가 숫자를 입력중이라면 숫자값을 설정해주는 함수
        if userIsIntheMiddleOfTyping {
            brain.setOperand(displayValue)
            userIsIntheMiddleOfTyping = false
        }
        
        if let mathematicalSymbol = sender.currentTitle {
            brain.perforOperation(symbol: mathematicalSymbol)
            displayValue = brain.result
        }
    }

 

Model에서 dictionary를 사용해 기호-action 으로 정리하자

var operations: Dictionary<String, Double> = [
        "𝝿" : .pi,
        "e" : M_E,
    ]
    
func perforOperation(symbol: String) {
    if let constant = operations[symbol] {
        accumulator = constant
    }
}

 

enum, switch 를 사용하여 코드를 명확하게하자.
* enum에 값과 함수가 필요하다고 정의하여 담아 전달할수 있다.

var operations: Dictionary<String, Operation> = [
        "𝝿" : Operation.Constant(.pi), // pi
        "e" : Operation.Constant(M_E), // M_E,
        "√" : Operation.UnaryOperation(sqrt), // sqrt
        "cos" : Operation.UnaryOperation(cos), // cos
    ]
    
enum Operation {
    case Constant(Double)
    case UnaryOperation((Double) -> Double)
    case BinaryOperation
    case Equals
}

func perforOperation(symbol: String) {
    if let operation = operations[symbol] {
        switch operation {

        //value - associatedConstantValue
        case .Constant(let value): accumulator = value
        case .UnaryOperation(let function): accumulator = function(accumulator)

        case .BinaryOperation:
            break
        case .Equals:
            break
        }
    }
}

 

Struct사용하기

- accumulator가 계속 저장됨을 이용하여 +,-,*,/ 같은 이항연산을 계산 할 수 있다.

- 계산기가 수행해야 할 함수와 기호를 누르기전 입력된 값(accumulator)을 전달 받는 Struct를 생성하여 peding 변수로 저장해준다.

- Equals나 다른 +,-,*,/를 눌렀을 때 pending에 저장된 계산기가 수행해야 할 함수를 실행한다.

func perforOperation(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            
            //value - associatedConstantValue
            case .Constant(let value):
                accumulator = value
            case .UnaryOperation(let function):
                accumulator = function(accumulator)
            case .BinaryOperation(let function):
                executePendingBinaryOperation()
                pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)
                
            case .Equals:
                executePendingBinaryOperation()
            }
        }
    }
    
    /// 준비된 이항연산 실행함
    private func executePendingBinaryOperation() {
        if pending != nil {
            accumulator = pending!.binaryFunction(pending!.firstOperand, accumulator)
            pending = nil
        }
    }
    
    private var pending: PendingBinaryOperationInfo?
    
    // Array, Double,Int,String 은 모두 struct, 값복사를 진행함
    struct PendingBinaryOperationInfo {
        var binaryFunction: (Double, Double) -> Double
        var firstOperand: Double
    }

 

Closure 사용하기

- 함수를 보다 쉽게 표현 할 수 있다.

- 기존 함수 코드에서 { 를 앞으로 보내고 { 자리에 in을 넣어주면된다.

- enum에서 데이터 타입이나 리턴 타입이 유츄가 가능하기 때문에 파라미터나 return 값을 생략 할 수 있다.

private var operations: Dictionary<String, Operation> = [
        "𝝿" : Operation.Constant(.pi), // pi
        "e" : Operation.Constant(M_E), // M_E,
        "√" : Operation.UnaryOperation(sqrt), // sqrt
        "cos" : Operation.UnaryOperation(cos), // cos
        // Operation에 정의한 ((Double, Double) -> Double)로 유추가 가능함.
        //
        "*" : Operation.BinaryOperation({ $0 * $1 }),
        "+" : Operation.BinaryOperation({ $0 + $1 }), 
        "-" : Operation.BinaryOperation({ $0 - $1 }), 
        "/" : Operation.BinaryOperation({ $0 / $1 }),
        "=" : Operation.Equals
    ]
    
// 1단계
// { 를 앞으로 보내고 in을 추가함
//    "*" : Operation.BinaryOperation({(op1: Double, op2: Double) -> Double in
//    return op1 * op2
//    })


// 2단계
// enum에서 double 타입임을 enum에서 알 수 있기때문에 제거가가능함.
//    { (op1, op2) -> Double in
//        return op1 * op2
//     }

// 3단계
// 인자이름으로 $0, $1이가능함
//    { ($0, $1) -> Double in
//        return op1 * op2
//     }
// 이렇게 되면 { return $0 * $1} 로 줄일 수가 있음
// enum에서 리턴값이 double임을 알기에 {$0 * $1} 으로 줄일 수 있음


enum Operation {
    case Constant(Double)
    case UnaryOperation((Double) -> Double)
    case BinaryOperation((Double, Double) -> Double)
    case Equals
}

 

전체소스코드

https://github.com/HOONITANG/calculatorMVC/tree/main/calculatorMVC

 

GitHub - HOONITANG/calculatorMVC

Contribute to HOONITANG/calculatorMVC development by creating an account on GitHub.

github.com

 

후기

앱 개발을 할 때 코드 패턴을 너무 몰랐던 것 같다.

리팩토링 할 엄두가 안난다..

원래는 스토리보드에 View를 그리기 때문에 View 생성코드가 따로 없는데 Controller에서 생성코드가 있으니까 지저분해 보이긴 한다.

근데 스토리보드로 하면 비슷한 View를 만들 때 너무 힘들어서 포기했다.

 

반응형