[Swift] swift 를 이용하여 계산기 어플리케이션 만들기

Hyebin Lee·2022년 6월 22일
0
post-thumbnail

장황한 서론

개인 프로젝트로 KnockKnock을 만들었더니 실제로 사용해볼 수 없냐며 만든 어플리케이션을 보여달라는 분들이 종종 계셨습니다.
하지만 저는 골수 애플 덕후라... 안드로이드기기를 평소에 들고 다니지 않아서 제가 개발한 어플리케이션을 들고 다니며 사용해볼 수 없었죠

이러한 이유로 어플리케이션을 못보여준다고 말하면
다들 너같은 애플 덕후가 왜 안드로이드로만 개발했냐, 원래 어플 개발은 직접 내 휴대폰 기기에서 사용해보는게 묘미다 라는 말들을 해주셨습니다.
공감하는게 졸업프로젝트로 프론트엔드 개발팀원분께서 만들어주신 어플리케이션이 실제로 안드로이드 기기에서 배포가 되고 딥러닝 팀원분의 휴대폰이 안드로이드여서 수업 때마다 보여주시며 같이 신기해하곤 했는데
확실히 내가 쓰는 휴대폰에 내가 개발한 어플을 넣어갖고 다니면서 수시로 사용하면 훨씬 재미도 있고 내 프로젝트에 더 애정이 생기는 것 같았습니다.

안드로이드 개발을 해야겠다고 결심했던 계기는 순전히 자바라는 언어를 더 연습하고 싶어서였는데 이제 질리도록 연습이 좀 된 것 같아서 내친김에 swift로 아예 기존의 프로젝트를 개발해보진 못하더라도 가벼운 어플리케이션을 개발하면서 입문해보는건 어떨까라는 생각으로 시작하게 되었습니다

스위프트라는 언어를 전혀 사용할 줄 몰랐는데 호기롭게 시작한 작은 프로젝트입니다.
요즘 kotlin을 공부하고 있는데 꽤나 비슷하다고 느꼈습니다(다행)
처음 이 언어를 공부하는 만큼, 가볍게 두 언어에 대해 비교하고 싶어져서 간단하게 비교해보겠습니다.

struct (Swift)와 data class (Kotlin)

스위프트라는 언어를 전혀 사용할 줄 몰랐는데 호기롭게 시작한 작은 프로젝트입니다.
요즘 kotlin을 공부하고 있는데 꽤나 비슷하다고 느꼈습니다(다행)
처음 이 언어를 공부하는 만큼, 개발하면서 느꼈던 kotlin과의 차이점을 간단히 정리하겠습니다.

  • kotlin 의 data class
data class Foo(var num: Int)
  • swift 의 struct
struct Foo {
var num: Int
}

가장 근본적인 차이는 swift의 struct는 클래스가 아닌 value타입 입니다.
따라서 값을 전달할 때 값 자체의 복사가 일어나기 때문에 defensive copy를 해주지 않아도 된다는 장점을 갖고 있습니다.

XCode에서 프로젝트 생성하기


APP개발이라 APP을 선택하면 위와 같은 창이 뜹니다.
본인이 원하는 프로젝트명과 Organization명 들을 설정해주면 됩니다.

View 구성하기

먼저 계산기에서 가장 중요한 숫자 버튼을 만들어보겠습니다.
버튼을 만드는 방법은 storyboard에서 버튼을 선택하여 생성하고 쉽게 커스터마이즈할 수 있습니다.
이후 해당 버튼이 클릭되었을 때의 기능을 추가적으로 코드로 구현해주면 됩니다.
추가적인 코드 구현은 Assistant Editor에서 진행합니다.
스토리보드에서 만들었던 버튼을 클릭하고 control키를 누른 채로 Assistant Editor작업창으로 드래그하면 자동으로 해당 버튼이 눌렸을 때 동작할 함수가 생성되며 해당 버튼은 그 함수와 매핑됩니다.


숫자 버튼 하나를 만들어 touchdigit 함수와 매칭시킨 뒤 스토리보드에서 버튼을 복사해 필요한 숫자 버튼을 여러개 생성해냅니다. 해당 버튼들도 모두 touchdigit 함수와 매핑되어 있습니다.

필요한 버튼을 위와 같은 절차에 따라 만들면 최종 View는 아래와 같이 만들 수 있습니다.
가장 위 진한 녹색 창은 display 창으로 계산결과나 입력값을 보여주는 창입니다.

display

가장 위 진한 녹색 창을 나타내는 부분입니다.
Controller 코드에서 아래와 같이 변수를 선언합니다.
이때 값이 null이 아니도록 !를 뒤에 붙입니다

@IBOutlet private weak var display: UILabel!

touchdigit

숫자를 터치할 때 계산기 특성상 고려해야 할 점이 있습니다.
소숫점이나(.)이나 0은 숫자 맨앞에 올 수 없다는 것입니다.
그에 따라 지금 타이핑하는 숫자가 첫번째 숫자인지 아닌지를 구분할 변수를 하나 선언하고 상황을 구분해주어야 합니다.

 private var userIsInTheMiddleOfTyping = false

touchdigit 메소드에서는 위에서 선언한 변수를 기반으로 첫번째 숫자라면 0과 소숫점(.)이 아닐 때만 display에 입력 숫자가 작성되도록 합니다.
첫번째 숫자가 아니라면 이전 display에 있던 값에 해당 입력 값을 덧붙여서 display에 보여줍니다.

 @IBAction private func touchdigit(_ sender: UIButton) {
        let digit = sender.currentTitle!
        if userIsInTheMiddleOfTyping {
            let textCurrentlyInDisply = display!.text!
            display!.text = textCurrentlyInDisply + digit
            print("middle digit = \(digit)")
        } else {
            if !(digit == "0" || digit == "."){
                display!.text = digit
            }
            print("first digit = \(digit)")
        }
        userIsInTheMiddleOfTyping = true
    }

String -> Double

display는 기본적으로 string 형태로 구현되어 있었습니다.
이를 값 연산을 위해 Double로 다뤄주는 변수를 따로 두겠습니다.

 //display value -> double type
    private var displayValue: Double {
        get { // displayValue의 값을 가져오기 위한 코드
            return Double(display.text!)!
        }
        set { // 누군가가 이 변수의 값을 설정하려고 할 때 실행되는 코드
            display.text = String(newValue)
        }
    }

연산 수행하기 (계산하기)

이제 연산을 위한 버튼들 (AC,사칙연산,음/양수 변환,파이 등)을 위한 함수를 만들겠습니다.
당연히 해당 버튼들은 touchdigit이 아닌 이 함수와 매칭되어 있어야 합니다.
원래 Controller layer에서 연산 수행 기능을 전부 구현해도 계산기 기능 자체는 문제 없이 작동 되지만
그러면 Controller layer가 너무 무거워져 나중에 유지보수나 기능확장이 힘들어지기 때문에
먼저 따로 기능 부분을 CalculatorBrain 클래스를 만들어 분리하겠습니다.

CalculationBrain

이 클래스는 값 연산을 따로 다루는 기능을 합니다.
먼저 입력된 숫자값이나 연산 결과를 저장할 수 있는 변수를 하나 선언합니다.

var accumulator: Double = 0.0

아무것도 연산이 안된 초기 시점에서의 값은 0이므로 초기값을 설정해줍니다.
이제 Controller로부터 피연산자를 받으면 그것을 accumulator 변수에 넣는 코드를 작성합니다.

 func setOperand(operand: Double) {
        accumulator = operand
        print("input value =\(accumulator)")
    }

이 계산기에서 다루는 연산은 AC,파이,+-,사칙연산 4가지, = 까지 총 8가지입니다.
이 모든 것을 조건문 하나하나 써서 구현하는 것은 코드 확장성에 좋지 않습니다.
따라서 해당 연산을 연산에 구현될 함수의 특징을 기준으로 아래와 같이 enum 타입으로 분류합니다.

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

enum 값뒤에 괄호로 각 연산마다 필요한 파라미터나 리턴값을 표시해줍니다.
필요한 파라미터와 리턴에 따라 기준을 상수, 단항연산, 다항연산, = 으로 분리했습니다.

이제 연산에 따라 실제 연산을 할 함수와 즉각적으로 mapping 되어있는 dictionary를 구현하겠습니다.
형태는 각 차례로 연산자 : 함수를 구현한 enum값 입니다.

  private var operations: Dictionary<String, Operation> = [
        "AC": Operation.Constant(0.0),
        "π": Operation.Constant(.pi),
        "+/-": Operation.UnaryOperation({$0 * (-1)}),
        "X": Operation.BinaryOperation({ $0 * $1 }),
        "÷": Operation.BinaryOperation({ $0 / $1 }),
        "+": Operation.BinaryOperation({ $0 + $1 }),
        "−": Operation.BinaryOperation({ $0 - $1 }),
        "=": Operation.Equals
    ]

이제 실제로 값을 계산하고 계산된 결과를 accumulator 변수에 넣는 코드를 구현하겠습니다
이때는 Controller와 소통하는 코드로써, 입력으로 직접 연산을 받아 연산에 해당하는 함수를 dictionary에서 찾고 계산된 결과값을 accumulator 변수에 넣도록 구현합니다.

  func performOperation(symbol: String) {
        if let operation = operations[symbol] {
            switch operation {
            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()
            }
        }
    }

여기서 한 가지 추가적으로 고려해야 하는 사항이 있습니다.
다항 연산의 경우 첫번째 피연산자에 연산을 추가해가는 식으로 구현이 되어야 합니다.
예를 들어서 1+2+3+4+5 연산을 하는 경우
1+2까지 수행하고 다음 + 연산이 입력되는 시점에서 1+2의 결과인 3이 피연산자가 되어야 합니다
마찬가지로 1+2+3을 마치고 다음 + 연산이 입력되는 시점에서는 1+2+3이 피연산자가 되어야 하고 상황이 반복됩니다.
= 연산자가 들어온 경우 더 이상 피연산자를 업데이트 하지 않고 가장 최근에 업데이트 된 피연산자로 연산된 값을 보여주면 됩니다.
그에 따라 코드는 아래와 같이 구현됩니다.

먼저 이항연산 함수와 피연산자를 저장하는 구조체를 만들고 해당 변수를 null로 초기화합니다.

  struct PendingBinaryOperationInfo {
        var binaryFunction: (Double, Double) -> Double // 이항함수
        var firstOperand: Double // 이항함수의 첫번째 피연산자를 추적
    }
    
    private var pending: PendingBinaryOperationInfo?

이항 연산에서 피연산자를 찾는 코드는 다음과 같이 구현합니다.

private func executePendingBinaryOperation() {
        if pending != nil {
            accumulator = pending!.binaryFunction(pending!.firstOperand, accumulator)
            pending = nil
        }
    }

그에 따라 위의 performOperation 코드의 이항연산 부분을 다시 보면,
이항 연산이 들어왔을 때 연산을 통해 accumulator에 업데이트하고,
다시 해당 내용을 피연산자로 만들기 위해 pending을 새롭게 설정하고 있습니다.

한편 equals에서는 더이상 피연산자를 새로 만들 필요가 없기 때문에 연산을 통해 accumulator를 업데이트하는 executePendingBinaryOperation만 수행합니다.

 case .BinaryOperation(let function):
                executePendingBinaryOperation()
                pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)
            case .Equals:
                executePendingBinaryOperation()
            }

performOperation

이제 완성된 CalculatorBrain 코드를 Controller에서 참조하면 됩니다.
참조 변수를 아래와 같이 생성합니다.

 private var brain: CalculatorBrain = CalculatorBrain()

그리고 기존에 만들어 둔 연산 버튼과 연산 코드가 연결되도록 연산 버튼에 매칭된 함수 내부에서 CalculatorBrain의 기능이 참조되도록 코드를 구현합니다.

  @IBAction private func performOperation(_ sender: UIButton) {
        if userIsInTheMiddleOfTyping { //상수값 입력을 하는 중에 연산 버튼을 눌렀다면
            brain.setOperand(operand: displayValue)// 상수값 보내기
            userIsInTheMiddleOfTyping = false // 상수값 입력을 그만받기
        }
        if let mathematicalSymbol = sender.currentTitle { //mathematicalSymbol이 들어오면
            brain.performOperation(symbol: mathematicalSymbol)
            //해당 연산자를 전송하여 연산 요청
        }
        displayValue = brain.accumulator
        //연산 결과값 가져오기
        
    }

기능이 많아서 전부 다 gif로 만들지는 못했지만 완성된 기능의 일부 시뮬레이션 화면입니다!

참고한 링크

https://small-thing.tistory.com/218?category=848022

0개의 댓글