03. 계산기 앱

곰주·2022년 8월 7일
1
post-thumbnail

계산기 앱

화면에는 계산 값 결과를 나타내 주는 Label과 계산기 키패드UI가 표시되도록, 그리고 기본적으로 사칙 연산과 누적 연산이 가능하고 AC 버튼을 누르면 계산기 초기화되도록 구현하였다.

프로젝트 들어가기 전 알아야 할 개념들

  1. UIStackView
  2. IBDesignalbes, IBInspectable

1. UIStackView

이 프로젝트에서 UIStackView를 활용하여 키패드 UI를 구현한 것이다. UIStackView는 열 또는 행에 View들의 묶음을 배치할 수 있는 간소화된 인터페이스이다. Stack View는 AutoLayout을 이용하여 디바이스의 스크린 사이즈 혹은 동적 변화에 맞추어 동적인 UI를 구성할 수 있다. 복잡한 UI를 구성하면 AutoLayout 제약 조건을 하나하나 설정하게 되어 제약 조건이 많아져 복잡해지고 관리하기 힘들어진다. 그래서 개발자가 원하는 대로 UI 구성이 안 될 수도 있단 말씀... 그래서! Stack View를 사용하는 것이다! Stack View를 사용하면 AutoiLayout 제약 조건을 많이 설정하지 않아도 쉽게 UI 구성이 가능하다!



1-1. UIStackView의 Attribute

1️⃣ Axis (축)

Axis는 Stack View의 가로 또는 세로 방향을 결정한다!
Verticla Stack View : 가로
Horizontal Stack View : 세로

2️⃣ Distribution (분배)

Distribution은 Axis를 따라 Stack View 안에 들어가는 View들의 사이즈를 어떻게 분배할지 설정하는 속성이다. Distribution 속성의 플래그에는 Fill, FillEqually, FillProportionally, EqualSpacing, EqualCentering이 있다. 자! 그럼 이제 이 5개의 플래그에 대해 알아보자!

(1) Fill

Stack View 방향에 따라 가능한 공간을 모두 채우기 위해 subView(Stack View 안에 있는 Stack View)들의 사이즈를 재조정한다. subView들이 Stack View의 크기를 초과한다면, 각 뷰의 compression resistance priority에 따라 각 크기를 감소시킨다. subView들이 Stack View의 크기에 미달한다면, hugging priority에 따라 각 View를 늘려 Stack View를 꽉 채우게 만든다. 따라서, 줄어들어야 할 때는 각 subView들의 compression resistance priority를 비교하여 우선 순위가 낮은 순위대로 크기를 감소시키고, 늘려야 할 때에는 hugging priority를 비교하여 우선 순위가 낮은 순위대로 크기를 증가시킨다.

근데.... compression resistance priorityhugging priority가 뭐지? 한번 알아보자!!

compression resistance priority
최소 크기에 대한 저항으로, compression(압축)에 대한 resistance(저항)이다. 숫자가 클수록 "나는 안 작아질 거야!!!"가 강해지는 것이다!

hugging priority
최대 크기에 대한 저항으로, 숫자가 클수록 "나는 안 늘어날 거야!!!"가 강해지는 것!!


(2) FillEqually

Stack View의 Axis(축)을 따라 가능한 공간을 채우기 위해 SubView들을 리사이징 한다. View들은 Stack View의 Axis를 따라 모두 같은 사이즈를 갖기 위해 재조정된다! 말 그대로, Stack View의 Axis를 따라 모두 같은 크기로 분배되도록 하는 옵션이다.


(3) FillProportionally

Stack View 방향에 따라 SubView가 갖고 있던 크기에 비례하려 공간을 차지하도록 만드는 설정이다. Stack View를 채우고 남은 공간이 생긴다면, intrinsic content size의 비율에 맞추어 공간을 분배하여 리사이징 된다고 한다!


(4) EqualSpacing

Stack View 방향에 따라 subView들 사이의 거리를 동일하게 만들어 주는 설정이다.


(5) EqualCentering

각 subView들의 센터와 그 센터 간의 거리를 동일하게 만들어 주는 설정이다.



3️⃣ Alignment (정렬)

Stack View의 Axis에 수직인 View들의 레이아웃을 정하는 속성이다. Stack View가 어떤 식으로 하위 View들을 정렬할 것인지에 대한 것이다! 이 속성에는 Fill, Leading, Top, First BaseLine, Center, Trailing, Bottom, Last BaseLine 총 7개의 플래그가 있다! 차근차근 알아보도록 하자.

(1) Fill

Stack View의 방향이 Horizontal일 경우, 상하 공간을 채우기(Fill) 위해 subView들을 늘리고, Vertical인 경우에는 좌우 공간을 채우기 위해 subView들을 늘리는 옵션이다!


(2) Lading

Vertical Stack View에서 subView들이 Stack View의 Leading에 맞추어 정렬하는 옵션으로, 왼쪽으로 정렬된다고 생각하면 된다.


(3) Top

Horizontal Stack View에서 subView들이 Stack View의 Top에 정렬되는 옵션이다.


(4) Fist BaseLine

subView들의 Fist BaseLine에 맞추어 Stack View가 subView들을 정렬하는 옵션으로, Horizontal Stack View에서만 사용 가능한 옵션이다.


(5) center

Stack ViewA의 방향에 따라 subView들의 Center를 Stack View의 Center에 맞추어 정렬하는 옵션이다.

(6) Tarailing

Vertical Stack View에서 Stack Viewdml Trailing에 맞추어 subView들을 정렬하는 옵션으로, 오른쪽으로 정렬한다고 생각하면 쉽다!


(7) Bottom

Horizontal Stack View에서 Stack View의 아래쪽(bottom)에 맞추어 subView들을 정렬하는 옵션이다.


(8) Last BaseLine

Stack View가 subView들의 Last BaseLine에 맞추어 subView들을 정렬하는 옵션이다. 이 정렬 또한 Horizontal Stack View에서만 가능한 옵션이다!



4️⃣ Spacing (여백)

Stack View 안에 들어가는 View들의 간격을 조정하는 속성으로, Spacing 값에 따라 subView들의 간격이 넓어지거나 좁아진다.



2. IBDesignalbes, IBInspectable

IB란... 🤨
스위프트에는 @IBOutlet, @IBInspectable, @IBDesignable와 같이 IB로 시작하는 Attribute들이 있는데, IB는 Interface Builder를 의미하고, 스토리보드를 말한다고 한다! 따라서, 스토리보드와 코드를 연결시킬 때, IB로 시작하는 Attribute들을 사용한다.

2-1. @IBInspectable

커스텀한 UIView 컴포넌트에서 인스펙터 창을 이용하여 쉽게 속성을 적용할 수 있도록 도와주는 속성이다. 쉽게 말해, 코드와 스토리보드의 인스펙터 영역을 이어주는 기능을 한다.

2-2. @IBDesignalbes

앞서 말한 @IBInspectable을 사용하여 속성들을 변경할 수 있지만, 빌드 전까지는 실제 모습을 확인하는 것은 불가능하다.... 그래서! 실제 모습을 확인할 수 있게끔 @IBDesignalbes을 사용하는 것이다. 스토리보드에서 생성한 컴포넌트와 소스코드를 연결하여 해당 객체의 현재 속성을 스토리보드에 실시간 반영하여 뷰를 그려 준다. @IBDesignalbes 클래스로 연결하면 실행하지 않고도 뷰를 정의한 모습 그대로 실시간 랜더링 하여 스토리보드에서 확인할 수 있다!



계산기 앱 완성!

StoryBoard


Code

ViewController.swift

import UIKit

enum Operation {
    case Add
    case Subtract
    case Divide
    case Multiply
    case unknown  // 아무 연산도 아니란 뜻
}

class ViewController: UIViewController {

    @IBOutlet weak var numberOutputLabel: UILabel!
    
    // View Controller의 계산기 상태 값을 가지고 있는 프로퍼티들
    var displayNumber = "" // 계산기 버튼을 누를 때마다 넘버 아웃풋 Label에 표시되는 숫자
    var firstOperand = ""  // 이전 계산 값을 저장하는 프로퍼티. 첫번째 피연산자
    var secondOperand = ""  // 새롭게 입력된 값을 저장하는 프로퍼티. 두번째 피연산자
    var result = ""  // 계산의 결과값을 저장하는 프로퍼티.
    var currentOperation: Operation = .unknown  // 현재 계산기에 어떤 연산자가 입력되어 있는지 알 수 있게 연산자 값을 저장하는 프로퍼티.
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }

    @IBAction func tapNumberButton(_ sender: UIButton) {
        // 계산기 숫자 버튼을 액션 함수로 연결하고 버튼을 눌렀을 때 누르면 입력 값이 표시되지 않는 문제에 대한 해결 소스코드
        guard let numberValue = sender.titleLabel?.text else { return }
        if self.displayNumber.count < 9 {
            self.displayNumber += numberValue
            self.numberOutputLabel.text = self.displayNumber
        }
        
        // sender.title : 선택한 버튼의 title 값을 가지고 옴
        guard let numberValue = sender.title(for: .normal) else { return }
        // 선택된 숫자값을 displayNumber에 문자열로 계속 추가될 수 있고, 9자리까지만 입력될 수 있게.
        if self.displayNumber.count < 9 {
            self.displayNumber += numberValue
            self.numberOutputLabel.text = self.displayNumber
        }
    }
    
    @IBAction func tapClearButton(_ sender: UIButton) {
        self.displayNumber = ""
        self.firstOperand = ""
        self.secondOperand = ""
        self.result = ""
        self.currentOperation = .unknown
        self.numberOutputLabel.text = "0"
    }
    
    @IBAction func tabDotButton(_ sender: UIButton) {
        // 숫자 8자리 입력하고 소수점 선택 뒤 숫자를 입력하면 소수점 포함 10자리가 표시되니까 소수점 포함 9자리가 될 수 있게 예외 처리
        // 소수점이 중복으로 찍히면 안 되니까 "."이 포함되지 않은 경우의 조건도 추가해 줌.
        if self.displayNumber.count < 8, !self.displayNumber.contains(".") {
            self.displayNumber += self.displayNumber.isEmpty ? "0." : "."
            self.numberOutputLabel.text = self.displayNumber
        }
    }
    
    @IBAction func tapDivideButton(_ sender: UIButton) {
        self.operation(.Divide)
    }
    
    @IBAction func tapMultiplyButton(_ sender: UIButton) {
        self.operation(.Multiply)
    }
    
    @IBAction func tapSubtractButton(_ sender: UIButton) {
        self.operation(.Subtract)
    }
    
    @IBAction func tapAddButton(_ sender: UIButton) {
        self.operation(.Add)
    }
    
    @IBAction func tabEqualButton(_ sender: UIButton) {
        self.operation(self.currentOperation)
    }
    
    // 연산자 버튼을 눌렀을 때 numberOutputLabel에 표시
    // 계산 함수
    func operation (_ operation: Operation) {
        if self.currentOperation != .unknown {  // 첫번째 피연산자와 두번째 피연산자를 연산
            if !self.displayNumber.isEmpty {
                self.secondOperand = self.displayNumber
                self.displayNumber = ""
                
                guard let firstOperand = Double(self.firstOperand) else { return }
                guard let secondOperand = Double(self.secondOperand) else { return }
                 
                switch self.currentOperation {
                case .Add:
                    self.result = "\(firstOperand + secondOperand)"
                    
                case .Subtract:
                    self.result = "\(firstOperand - secondOperand)"
                    
                case .Divide:
                    self.result = "\(firstOperand / secondOperand)"
                    
                case .Multiply:
                    self.result = "\(firstOperand * secondOperand)"
                    
                default:
                    break
                }
                
                if let result = Double(self.result), result.truncatingRemainder(dividingBy: 1) == 0 {
                    self.result = "\(Int(result))"
                }
                
                self.firstOperand = self.result
                self.numberOutputLabel.text = self.result
            }
            
            self.currentOperation = operation
        }
        else {  // unknown이라면, 계산기가 초기화된 상태에서 사용자가 첫번째 피연산자와 연산자를 선택한 상태일 것임.
            self.firstOperand = self.displayNumber  // 화면에 표시된 숫자가 첫번째 피연산자가 될 것임.
            self.currentOperation = operation  // 선택한 연산자 저장.
            self.displayNumber = ""
        }
    }
}

RoundButton.swift

import UIKit

// 변경한 값을 실시간으로 스토리 보드에서 볼 수 있게 @IBDesignable 선언
@IBDesignable
class RoundButton: UIButton { // UIButton을 상속한 RoundButton. 기존 UIButton 들의 속성들을 그대로 사용 가능, 추가적으로 사용자가 원하는 속성들을 이 클래스에 만들어 사용 가능.
    @IBInspectable var isRound: Bool = false {  // 스토리 보드에서도 isRound의 설정 값을 변경할 수 있게 @IBInspectable
        didSet
        {
            if isRound
            {
                self.layer.cornerRadius = self.frame.height / 2  // 정사각형 버튼 : 원, 정사각형이 아닌 버튼은 모서리가 둥글게
            }
        }
    }
}

실행 화면



패스트 캠퍼스
사이트

profile
아기코쟁이 🧑🏻‍💻

0개의 댓글