iOS) 계산기 앱 (1)
Angela 강좌 코스 중 계산기 앱을 만든 적이 있다.
정확히 말하면 계산기 UI를 이용한 오토 레이아웃 연습이었다. 실제 계산 기능은 없었던 오토 레이아웃 연습만을 위한 앱이었는데,
이번에는 계산기의 기능을 할 수 있는 파트를 수강 중이다.
오토 레이아웃은 이미 적용되어 있는 상태인데 오랜만에 계산기를 보니 반가웠다.
한창 처음 공부할 때 계산기 오토 레이아웃에서 애를 먹은 적이 있는데 Stack View가 주원인이었다.
이번 포스트에서는 오토 레이아웃에 대한 설명은 하지 않을 거지만 하나의 팁을 남기자면 먼저 가로줄(ex. [AC, +/-, %, ÷], [7,8,9]...)에 대한 Stack View를 먼저 설정해주고 그 Stack View들을 세로로 Stack View를 만들어주면 된다!
코드 부분을 보면 IBAction이 만들어져 있다.
calcButtonPressed - 연산자 버튼에 대한 IBAction
numButtonPressed - 숫자 버튼에 대한 IBAction
먼저 numButtonPressed에 코드를 작성해보자.
numButtonPressed
각 숫자를 클릭하면 코드에서는 어떤 숫자가 클릭되었는지 알 수 있는 정보가 필요하다.
우리는 이미 title을 숫자로 정해놓았기 때문에 버튼의 title을 이용하여 정보를 가져올 수 있다.
sender는 정말 편한 메서드들을 제공해주는데 여기서 currentTitle이 제격으로 보인다.
하지만 위에 보이는 titleLabel과의 정확한 차이점이 궁금해 이렇게 기록하게 되었다.
titleLabel
애플에서 정의한 titleLabel은 버튼의 currentTitle 속성 값을 표시한다고 되어있다.
음.. 쉽게 말해서 titleLabel.text처럼 보통 title에 접근해서 속상 값을 변경할 때 자주 쓰인다.
currentTitle
반면에 currentTitle은 "읽기 전용"이다.
titleLabel은 내부 메서드에 접근해 값을 변경할 수 있는 반면에 currentTitle은 단순히 읽어올 때 쓰여서 현재 계산기에서는 currentTitle이 더 적합하다.
(설명이 부정확할 수도 있습니다. 양해 부탁드리며 수정 댓글 반갑게 받겠습니다.)
@IBAction func numButtonPressed(_ sender: UIButton) {
//What should happen when a number is entered into the keypad
if let numValue = sender.currentTitle {
displayLabel.text = numValue
}
}
옵셔널 바인딩을 사용하여 nil값이 나올 때 앱이 crush 되는 걸 막아주었다.
현재 이 코드를 이용해 앱을 실행시키면 내가 클릭한 숫자 버튼의 숫자가 잘 표시는 되지만, 버튼을 누를 때마다 numValue가 달라지다 보니 숫자가 한 자릿수만 표시가 된다. ex) 1을 누른 후 5를 누르면 15가 아닌 5만 표시된다.
Angela는 이에 대해서 challenge를 내주었는데 아래는 내가 해결한 방법이다.
var numStack = ""
@IBAction func numButtonPressed(_ sender: UIButton) {
//What should happen when a number is entered into the keypad
if let numValue = sender.currentTitle {
numStack += numValue
displayLabel.text = numStack
}
}
String타입인 numStack을 전역 변수로 설정하고 새로운 button이 클릭될 때마다 더해주었다.
그 결괏값을 displayLabel.text에 표시하여 현재까지 누른 숫자 버튼의 title이 저장되어 보인다.
아래에는 Angela가 제시한 방법이다.
private var isFinishedTypingNumber: Bool = true
@IBAction func calcButtonPressed(_ sender: UIButton) {
//What should happen when a non-number button is pressed
isFinishedTypingNumber = true
}
@IBAction func numButtonPressed(_ sender: UIButton) {
//What should happen when a number is entered into the keypad
if let numValue = sender.currentTitle {
if isFinishedTypingNumber {
displayLabel.text = numValue
isFinishedTypingNumber = false
} else {
displayLabel.text = displayLabel.text! + numValue
}
}
}
먼저 bool타입의 isFinishedTypingNumber를 true로 선언을 해준다.
private으로 선언해 준 이유는 숫자 버튼이 눌릴 때 기능을 구현하는 부분은 현재 class에서만 이루어져야 하기 때문에 다른 파일에서의 접근을 차단하기 위해서다.
그리고 숫자 버튼을 누르면 처음 누른 숫자가 표시됨과 동시에 값을 false로 바꾸어준다.
그 후에도 숫자를 추가로 누르면 false이기 때문에 else문으로 이동해 기존의 displayLabel.text에 새 값을 추가해준다.
isFinishedTypingNumber는 해석하면 숫자 타이핑이 끝났냐 라는 의문문인데, 생각해보면 숫자를 다 누르고 연산기호들을 누른다. 그러면 숫자 타이핑이 끝났다!라고 볼 수 있기에 calcButtonPressed가 호출될 때 값을 true로 바꾸어준다.
기능 추가 (1)
사칙연산을 적용하기 전에 먼저 맨 윗줄의 세 개의 버튼에 대한 기능을 추가해보자.
calcButtonPressed
@IBOutlet weak var clearBtn: UIButton!
private var isFinishedTypingNumber: Bool = true
private var displayValue: Double {
get {
guard let number = Double(displayLabel.text!) else {
fatalError("Cannot convert display label text to a Double.")
}
return number
}
set {
displayLabel.text = String(displayValue * newValue) // 방법 1
// displayLabel.text = String(newValue) 방법 2
}
}
// 연산버튼
@IBAction func calcButtonPressed(_ sender: UIButton) {
isFinishedTypingNumber = true
if let calcMethod = sender.currentTitle {
switch calcMethod {
case "+/-":
displayValue = -1 // 방법 1
// displayValue *= -1 방법 2
case "C":
displayLabel.text = "0"
clearBtn.setTitle("AC", for: .normal) // C 버튼을 누르면 다시 AC로 바뀜.
case "%":
displayValue = 0.01 // 방법 1
// displayValue *= 0.01 방법 2
default:
displayLabel.text = "0"
}
}
}
private var displayValue: 앞의 포스트에서 computed property에 대해 공부해보았는데 계산기 앱에 응용을 해봤다.
(방법 1은 내가 작성한 코드, 방법 2는 angela의 코드다. 큰 차이는 없지만 방법 2가 다른 사람이 읽을 때 가독성이 더 있어 보인다.)
get - get에서는 number라는 상수를 guard let 구문으로 생성한다. number는 숫자가 표시되는 화면에 나오는 숫자를 Double 타입으로 캐스팅해주는데 그 이유는, String 그대로 사용하게 되면 어떠한 수학적 연산을 할 수 없기 때문이다.
그 후 우리가 만든 number를 return 해 준다.
set - +/-, % 버튼을 클릭할 시, 양수나 음수로 바꾸어주거나 퍼센티지로 나타내 주는 연산 작업을 하는데 연산 프로퍼티를 사용함으로써 코드의 재활용을 할 수 있는 모습을 볼 수 있다.
+/-, % 연산에 연산 프로퍼티 적용하기
case "+/-":
displayLabel.text = String(displayValue * -1)
case "%":
displayLabel.text = String(displayValue * 0.01)
연산을 위해 String으로 들어온 숫자를 Double로 캐스팅한 displayValue에 각 연산에 해당하는 수를 곱해준 후 다시 String으로 캐스팅하여 displayLabel.text에 넣어준다.
프로젝트 자체가 작아서 굳이 연산 프로퍼티를 사용하지 않아도 된다고 생각할 수 있지만, 중복되는 코드를 최대한 안 만드는 것이 효율적이라 생각함과 동시에 만약 이러한 case문이 수백수천 개가 된다면? 하는 생각에 연산 프로퍼티를 사용하면 중복되는 코드를 확 줄일 수 있다.
case "+/-":
displayValue = -1
case "%":
displayValue = 0.01
이렇게 배운 것을 실제 상황에 대입하여 응용해보니 더 이해가 잘되는 느낌이 있다 ㅎㅎ
나머지 case
case "C":
displayLabel.text = "0"
clearBtn.setTitle("AC", for: .normal)
default:
displayLabel.text = "0"
C(clear): C 버튼을 누르면 계산기 화면의 숫자가 0으로 초기화되며 outlet으로 선언한 버튼의 title을 "AC"로 바꾸어준다.
이에 대한 내용은 아래에서 더 자세하게 설명되어있다.
default : 모든 case에 대해서 작성하지 않았으므로 default를 사용하여 기본값을 주어야 한다. 여기선 "0"으로 해주었다.
사실 angela는 if문을 사용하여 코드를 작성하였는데 나는 갑자기 switch문이 사용하고 싶어서 이대로 진행했다. :)
numButtonPressed
// 숫자버튼
@IBAction func numButtonPressed(_ sender: UIButton) {
clearBtn.setTitle("C", for: .normal) // 숫자를 누르면 AC -> C로 바뀜.
if let numValue = sender.currentTitle {
if isFinishedTypingNumber {
displayLabel.text = numValue
isFinishedTypingNumber = false
} else {
displayLabel.text = displayLabel.text! + numValue
}
}
}
먼저 calcButtonPressed에서도 보았던 AC와 C가 계산기에서 무엇을 의미하는지부터 간단하게 짚고 넘어가자!
AC, C, CE
All Clear의 약자로 모든 계산 메모리를 지운다. 숫자를 입력하기 시작하면 AC는 C로 바뀌며 C는 입력한 식을 지우는데
예를 들어 첫 번째 식만 작성할 때는 AC나 C나 모두 지우는 동일한 기능을 하지만 2 +?처럼 사칙연산을 입력 후? 에 숫자를 입력했을 때
C는 방금 입력한 식 즉,? 만 지운다.
사실 CE라는 Clear Entry가 방금 언급한 방금 기입한 식만을 지우는 역할을 하는 계산기도 있지만 아이폰 계산기에서는 CE라는 버튼은 따로 없고 AC와 C만 존재한다.
정리)
AC - AllClear
C - Clear
CE - Clear Entry
애플 계산기에서는 CE버튼은 따로 없고 C가 그 기능을 같이 수행함.
다시 코드로 넘어와보자.
애플 계산기를 써보면 알겠지만 숫자를 누르기 시작하면 "AC"버튼이 "C"로 바뀌는 걸 볼 수 있다.
첫 줄의 코드는 setTitle을 통해 title을 변경해준 것이다.
다음 if-else문은 위에서 작성한 코드와 비슷하지만 한 가지 기능을 더 추가했다.
바로 소수점에 대한 기능이다.
알다시피 displayLabel.text는 String type이다.
그래서 수학적으로 말이 안 되는 2.1.3.4 같은 숫자도 입력이 가능한데 이상태에서 연산을 하게 되면 app이 crash 된다.
그리하여 소수점에 대한 기능을 추가하게 되었다.
먼저 numValue(입력된 값)이 소수점이면 먼저 displayValue(현재까지 입력한 수)가 정수인지 아닌지 판단한다.
판단기준:
displayValue == 3일 때 -
floor 함수는 소수점 버림 함수이다. displayValue가 아직 소수점을 작성하지 않은 3이라는 정수라면 floor(3) == 3에 따라
isInt는 true를 반환하고 소수점을 추가한다.
displayValue == 3.1일 때 -
같은 방법으로 정수 판단을 하는데 소수점을 작성한 3.1이라는 소수라면 floor(3.1)은 3이 되는 반면, 비교 대상인 displayValue는 3.1이기 때문에 false를 반영하여 소수점 버튼을 누르더라도 어떠한 반응도 하지 않는다.
오류 발견
화면에 보이는 것 처럼 소수점을 입력한 후 하나의 숫자를 더 입력하고 나서 소수점을 추가하려고 할 때는 추가가 안되지만,
소수점 입력 후 다시 소수점을 입력하면 입력이 된다.
이는 판단 기준에서 displayValue로만 판단을 했기 때문에 좀 더 직관적이고 디테일한 방법으로 코드를 다시 작성하였다.
if numValue == "." {
guard !displayLabel.text!.contains(".") else {
return
}
}
소수점 입력이 들어왔을 때 displayLabel.text가 소수점을 포함하는지 판단하여 포함한다면 return문을 통해 소수점 추가를 막고 소수점이 없다면 그대로 소수점을 추가해준다.
훨씬 간결하고 직관적인 방법이다.