SwiftUI 로 계산기를 만들어보고 이를 통해 배운 내용을 정리하였다.
두 가지 방식으로 구현하였다.
1. 직접 calculate() 함수를 만드는 방법
2. NSExpression 을 사용한 방법

1. 직접 calculate() 함수를 만드는 방법
먼저, 직접 calculate 함수를 만드는 방법은 다음과 같다.
1. View
먼저, 계산기에 들어갈 button view 를 만든다.
계산기의 버튼은 다음과 같이 [String] 으로 만들어서, view 안에서는 중첩 Foreach 문을 사용하여 레이아웃을 만들 수 있다.
디스플레이에 표시할 계산기의 결과는 inputValue로 설정하였다.
<코드를 보기 전, 알아야 할 개념 정리>
1. @State
inputValue 는 @State (상태 프로퍼티) 로 선언하였다.
계산기의 버튼을 눌러 해당 변수의 값이 변경될 때마다 뷰가 자동으로 업데이트 되어 변경된 값을 반영하게 한 것이다.
2. ForEach
ForEach 문의 정의는 다음과 같다.
struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable
- Data: RandomAccessCollection 프로토콜을 따르는데, 이는 배열이나 범위 등과 같은 컬렉션이 들어갈 수 있다.
- ID: Hashable (해시 함수를 통해 고유한 해시값을 만드는데 이를 통해 해당 객체를 유일하게 식별할 수 있기에, 집합이나 딕셔너리의 키로 사용이 될 수 있다. ) 을 따른다.
컬렉션 내 각 요소를 유일하게 식별할 수 있도록 하는 게, ID 인 것이다.
ForEach 에 id: \.self 와 같이 쓸 수 있는데, 이를 풀어서 설명하면
\는 키 패스의 시작을 나타내고, .self는 반복되는 요소 자체를 의미한다.
즉, id: \.self는 ForEach 구문이 반복하는 각 요소(여기서는 row)를 고유하게 식별하기 위해 요소 자체의 값을 사용하라는 의미.
buttons 는 중복이 되지 않게 선언했기에 저렇게 쓴 것.
3. Closure
이어서 ForEach 안에서 { row in ... } 이런 식의 코드는 클로저를 나타낸 것.
{ (매개변수 목록) -> 반환 타입 in
// 클로저가 수행할 코드
}
클로저 내부에서 row 와 같은 파라미터를 사용하여, 데이터 처리나 뷰를 구성할 수 있는 것이다.
struct ContentView: View {
@State var inputValue = "0"
let buttons = [
["7", "8", "9", "/"],
["4", "5", "6", "*"],
["1", "2", "3", "-"],
[".", "0", "C", "+"],
["="]
]
var body: some View {
VStack {
Text(inputValue)
.frame(maxWidth: .infinity, alignment: .trailing)
.font(.system(size: 48))
.padding()
.background(Color.gray.opacity(0.2))
ForEach(buttons, id: \.self) { row in
HStack {
ForEach(row, id: \.self) { buttonChar in
Button(action: {
// buttonTapped(buttonChar)
}) {
Text(buttonChar)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.font(.system(size: 48))
.foregroundStyle(buttonChar == "C" ? .red : .black)
.border(Color.gray, width: 1)
}
}
}
}
}
}
그리고 이제 계산기 버튼을 눌렀을 때 실행할 함수를 만든다.
2. func buttonTapped(_ button: String)
처음 설정한 inputValue 가 "0" 이므로, 뜻하지 않은 "0"을 처리하는 코드를 추가하였고
switch 구문을 통해 각각 계산을 할지, 초기화를 할지 설정하였다.
또한 "2++" 과 같이 사용자가 연산자를 연속으로 누른 경우, +, -, *, / 가 두 개 이상 나와서는 안되므로,
contains(inputValue.suffix(1)) 를 통해 가장 마지막 문자가 연산자일 경우, 해당 글자를 remove 하도록 하였다.
func buttonTapped(_ button: String) {
if(inputValue == "0") {
inputValue = ""
}
switch button {
case "=":
calculate()
case "C":
inputValue = "0"
case "+","-","*","/":
if ["+", "-", "*", "/"].contains(inputValue.suffix(1)) {
inputValue.removeLast()
}
inputValue += button
default:
inputValue += button
}
}

이제 buttonTapped 에 이어서, 입력한 수식을 처리하는 로직을 구성한다.
3. Calculate()
func calculate() {
let operators: [Character] = ["+", "-", "*", "/"]
if operators.contains(inputValue.suffix(1)) {
inputValue = "Invalid Input"
return
}
var numbers = [Double]()
var currentNumber = ""
var currentOperator = "+"
for char in inputValue {
print("-----")
print(char)
if operators.contains(char) {
if let number = Double(currentNumber) {
switch currentOperator {
case "+":
numbers.append(number)
case "-":
numbers.append(-number)
case "*":
numbers[numbers.count - 1] *= number
case "/":
numbers[numbers.count - 1] /= number
default:
break
}
currentNumber = ""
currentOperator = String(char)
}
} else {
currentNumber += String(char)
}
}
if !currentNumber.isEmpty {
if let number = Double(currentNumber) {
switch currentOperator {
case "+":
numbers.append(number)
case "-":
numbers.append(-number)
case "*":
numbers[numbers.count - 1] *= number
case "/":
numbers[numbers.count - 1] /= number
default:
break
}
}
}
inputValue = String(numbers.reduce(0,+))
}
우선 사용될 수 있는 연산자를 먼저 초기화한다. 우리가 구현하려고 있는 기능을 이렇게 선언 해보는 습관을 들여보자.
만약 수식이 연산자로 끝나면, 이는 유효하지 않은 입력으로 간주되기에 입력을 검증하는 과정을 추가하였다.
계산에 사용될 숫자들을 담을 수 있는 Double 형으로 선언한 배열을 초기화하고,
현재 처리하고 있는 숫자, 연산자를 초기화한다.
숫자가 입력되는 동안 currentNumber 변수에 누적되며, 연산자를 만나면 이 숫자가 numbers 배열에 추가될 것이다.
이제 본격적으로 사용자가 입력한 수식을 문자별로 순회하도록 한다.
우선, 현재 순회하는 문자가 연산자인지를 확인하고, 연산자라면 현재 처리하고 있는 숫자를 Double 형으로 변환한다.
변환이 성공하면, switch currentOperator 를 통해 '현재 연산자'에 따라 각기 다른 계산을 수행을 하도록 한다.
연산자가 +, - 라면 해당 number 를 적절히 처리하여 numbers 배열에 append.
연산자가 *, / 라면, numbers 배열의 마지막 숫자를 *, / 연산한 값으로 업데이트 시킨다.
switch 문이 끝나면, currentNumber와 currentOperator는 다음 숫자와 연산자의 처리를 위해 변수를 초기화한다.
그 다음으로,
if !currentNumber.isEmpty.
즉, 마지막으로 누적된 숫자가 처리되지 않았는지 확인해야 한다.
만약 처리되지 않은 숫자가 있으면, 이 또한 Double 로 변환 후, switch 문에 따라 적절히 연산을 수행한다.
마지막으로 계산에 사용될 숫자들을 담은 배열,
numbers 에 저장된 모든 숫자들을 reduce(0, +) 를 통해, 배열에 있는 모든 요소를 합하였다.
2. NSExpression 을 사용한 방법
NSExpression 클래스를 사용하여 계산을 수행하면, 위 코드를 아주 간단하게 구현할 수 있다.
NSExpression은 Foundation 프레임워크의 일부로, 문자열 형태의 표현식을 분석하고 계산할 수 있게 해준다.
위와 같이 inputValue 를 사용하여 NSExpression 객체를 생성한 후, 메서드를 호출하여 표현식을 계산하도록 한다. Double 로 타입 캐스팅 하여 value 에 저장하면 끝이다.
func calculate() {
let expression = NSExpression(format: inputValue)
if let value = expression.expressionValue(with: nil, context: nil) as? Double {
inputValue = String(value)
} else {
inputValue = "Invalid input"
}
}