저번주에 여기까지 하고 마무리를 했다.
버튼을 눌렀을 때 해당 버튼이 눌리는 것 까지 프린트로 확인했으니,
오늘은 숫자를 눌렀을 때 화면에 숫자를 띄워주고 등등 레벨 8단계까지 해보려한다.
자 가보자아아아
일단 숫자를 치면 label에 나오게 해봐야할 것 같다.
저번에 만들어놓은 buttonTapped
메서드에 코드를 추가 했다.
label.text
는 label 화면에 표시되는 텍스트를 담당하는 UILabel
객체다보니 label.text는 표시된 텍스트 값을 반환하게 된다.
그리고 ?? ""
해당 연산자는 닐 병합 연산자 라고 하는데, label.text
가 nil
일 경우 빈 문자열""
으로 사용한다는 뜻.
저번에 sender
적으면서 알게 됐다.
만약에 nil
값이 나오면 currentText
는 빈 문자열로 초기화되니,
혹시라도 텍스트가 없을 경우를 처리하는 안전장치로 보면 된다.
@objc private func buttonTapped(sender: UIButton) {
var currentText = label.text ?? ""
var buttonText = sender.title(for: .normal) // 클릭된 버튼 텍스트 가져오기
}
그리고 sender.title(for: .normal)
이 메서드가 클릭된 버튼의 텍스트를 가져오는 역할을 한다.
normal
은 그냥 버튼의 기본 상태를 의미하는 것인데,
버튼이 일반적인 상태일 때 텍스트를 가져오는 것을 말한다.
전부터 느낀건데 sender
가 끌어서 가져오는 역할을 하는게 신기하다.
아 근데 가져온다는 것 보단.. @objc
메서드로 전달이 되면 그 버튼이 sender
가 되는 것인데,
그렇게 되면 sender
가 버튼 객체 자체를 가리키게 되어서 버튼에 관련된 속성을 참소할 수 있게 해준다고 한다.
이렇게 currentText
와 buttonText
를 더하면 숫자가 보일거라 생각했는데
숫자가 나오지 않고 버튼이 눌렸다는 프린트만 출력됐다.
그래서 의아해하며 뭐지 하고 보다가
아! 라벨 텍스트를 업데이트 해야하는구나 하고 깨달았다.
마치 코테 풀 때 return
을 하는 것처럼...
그렇게 라벨 텍스트를 currentText
로 업데이트를 해주니,
텍스트가 잘 나온 걸 확인했다. 굿..
이제 앞에0
이 오는걸 막아야하는데,
예전에 코테를 풀 때 hasprfix
라는 메서드를 사용했던 기억이 났다.
위 블로그를 봤었는데 무튼 구간 중 일치 여부를 확인해주는 메서드여서
일치하는 것을 찾아내어 그걸로 조건문을 걸 수 있었다.
그래서 hasprefix
를 이용해 맨 앞이 0
과 일치하는것과,
currentText
의 카운트, 즉 숫자 갯수가 1개 미만일 경우까지 둘 다 참일 경우
removeFirst
라는 메서드로 첫번째 요소를 제거할 수 있다.
이 메서드는 컬렉션에서만 사용이 가능한 메서드인데, 전에 컬렉션 공부하다가 얻어걸린 메서드다.
removeFirst
도 있고 그때 같이 공부한걸로 removeLast
도 있다.
두 메서드는 둘 다 수정 가능한 컬렉션 (배열 또는 문자열) 에서 사용할 수 있다.
이렇게 이제 앞에 0이 오지 못한다.
추후 연산자도 두 번씩 입력되지 못하게 추가하고 싶은데 우선 지금은 빨리 진도부터 나가자 ㅠㅠ
우선 레벨별로 기능을 다 추가하고 그 다음에 좀 더 디테일한 부분을 수정하면 될 것 같다.
초기화 버튼은 이미 레벨 1~5 사이에 구현해놔서 딱히 할 것이 없었다.
바로 레벨 Lv.8로 넘어가보자
아 연산도 직접 해야하나 했는데, 천사 과제라서 연산에 필요한 메서드가 제공되었다.
물론 다른 분들은 직접 연산을 구현하시는 분들도 계셨지만,
난 그냥 이거 쓸래요.
만들어내고나서 구현해볼만 하다! 하면 구현하는거고 ..!
이제 result (=)
이 버튼이 기능을 실행해야할 때인 것 같다.
그리고 연산자 버튼들도 연산 역할을 해야하니 그 부분도 추가 해주어야할 듯 하다.
우선 가드 렛 구문으로 label.text
에서 텍스트를 잘 가져와 준 뒤,
텍스트가 nil
인 경우 return
해서 메서드 종료를 시켰다.
언래핑을 해준 이유는 label.text
가 String?
타입이라서 그랬다.
아까 숫자 버튼 액션 할 때도 그래서 ?? ""
을 사용해준 것이고,
지금은 연산 조건을 걸어야해서 guard let
을 사용한 것이라 보면 된다.
다시 한번 되새기자면 ??
은 옵셔널 병합 연산자다.
값이 nil
일 경우 기본값을 제공하게 되는 것, ?? ""
은 nil
일 경우 빈 문자열을 대신 사용하겠다는 의미!
꼭 기억하자.
아까 currentText
로 텍스트 가져왔는데 왜 또 가져오지? 이 생각을 할 수 있는데,
다시 operatorText
라는 이름의 텍스트로 가져오는 이유는 연산자 버튼에서 눌린 텍스트를 얻기 위해서 였다.
아 둘 다 똑같은거 아니야? ... 솔직히 난 구분 못했었다.
즉 label.text
는 "12"일 수도 있고, "12 + "일 수도 있는 것이고,
sender.title(for: .normal)
은 누른 버튼의 텍스트만 가져오는 것.
예를 들어, +
버튼이 눌렸다면 sender.title(for: .normal)
은 +
인 것이다.
문자열 결합 후 에러가 났다.
"돌연변이 연산자의 왼쪽은 변경할 수 없습니다: 'currentText'는 'let' 상수입니다"
습관처럼 가드렛 가드렛 했는데 guard var
였나보다..
여기서 다시 왜 guard var
인지 차이를 보자면,
guard let
은 상수
를 바인딩하는 데 사용. 즉, 값을 변경하지 않겠다고 선언하는 것.guard var
은 변수
를 바인딩하는 데 사용하며, 해당 값을 나중에 변경할 수 있도록 허용.코드에서 currentText
는 label.text
기반으로 만들어지지만,
연산자를 추가해서 currentText
값을 수정해야하기 때문에 var
로 해야했던 것이다.
let
으로 사용하면 값을 수정할 수 없으니!
guard var currentText = label.text else { return }
/* 위에서 currentText는 label.text의 값이 들어오고
그 값은 변경이 가능해야 하기 때문에 `var`로 바인딩 해야한다.
*/
자 그렇게 var
로 변경해서 오류는 없어졌고 완료가 됐다.
그렇게 label.text = currentText
까지 써주어 연산자도 나오게 해주었다.
과제에 있는 수식 문자열을 넣으면 계산해주는 메서드를 이용했다.
이 코드도 풀어서 해석해보니 정수Int
결과만 반환하고, 입력 문자열이 정확한 수식어야했다.
이것보다 더 복잡한 계산은 지원하지 않는다는 것.
결과버튼 구현은 위 코드로 작성했다.
첫 번째 if let
에서는 label.text
가 nil
인지 확인해야 했다.
아까도 말했지만 label.text
는 옵셔널이라 값이 없을 수도 있기 때문.
만약 nil
이 아니면 currentText
에 안전하게 바인딩을 하고 다음 작업을 수행하기 위해서다.
그리고 두번째 if let
은 calculate(expression:)
메서드가 반환하는 값이 nil
인지 확인하고 실패하면 nil을 반환해야한다.
값이 잘 나오면 result
에 잘 바인딩 할 것이고 결과적으로 label.text
에 잘 표시해 줄 것이다.
만약 실패하면 Error
를 표시 할 것이고.
이렇게 두번 나눠서 사용한 이유는 첫번째 if let
이 label
존재 여부를 확인한 뒤,
두번째 if let
이 계산이 성공했는지 확인하기 위함이었다.
이렇게 분리를 해야 내가 이해하기가 쉬웠다. 내 딴에 코드 가독성을 높인 것이라 보면 되겠다.
1 + 2
를 해야하는데 1 ++ 2
를 할 수도 있으니 이 부분을 막아보려한다.
let operators: Set<Character> = ["+", "-", "*", "/"]
if let last = currentText.last, operators.contains(last) {
return
}
우선 연산자를 Set
으로 묶어준다.
Set
을 사용하는 이유는 연산자를 효율적으로 확인할 때
배열보다 Set
이 contains
메서드를 사용할 때 더 빠르기 때문이다.
그리고 currentText.last
로 문자열의 마지막 문자를 샥 가져와주고
currentText
가 비어있지 않으면 마지막 문자를 넣고, 비어있다면 nil
을 반환해야한다.
contains
는 last
가 연산자 중 하나라면 true
를 반환할거고, 그렇지 않으면 false
를 만환할 것이다.
return
을 했는데 만약 last
가 연산자라면 함수나 메서드의 나머지 부분을 중단해준다.
연산자 중복 클릭 안되게 막기 성공
아까 Set
이 배열보다 contains
메서드를 사용할 때 더 빠르다는 말을 했는데,
시간 복잡도와 관련이 있다.
[ 배열(Array)에서 contains 사용 시 ]
배열은 순차적인 자료구조인데,
배열에 있는 항목들을 처음부터 끝까지 하나씩 확인해야 해서contains
메서드를 사용할 때
선형 탐색(linear search)
을 한다.
배열의contains
메서드의 시간 복잡도는O(n)
이다.
배열의 크기n
에 비례해 시간이 걸리고,
배열이[1, 2, 3, 4, 5]
일 때contains(3)
을 호출하면,
배열의 첫 번째부터 끝까지 3이 있는지 확인해야 하니.. O(n)의 시간이 더 걸리는 것.
[ 셋(Set)에서 contains 사용 시]
셋은 해시 기반 자료구조다.
내부적으로 해시 테이블을 사용하여 데이터를 저장하고 검색하기 때문에,
데이터를 찾을 때 효율적인해시 탐색(hash search)
방식을 사용한다고 한다.
셋의contains
메서드의 시간 복잡도는O(1)
인데,
셋에 있는 항목을 찾는 데 걸리는 시간이 입력된 데이터의 크기와 무관하게 일정하다.(평균적으로)
셋은 데이터가 삽입될 때 해시 함수에 의해 각 항목에 고유한 해시 값을 부여하고,
이 해시 값을 통해 데이터를 빠르게 찾을 수 있는 것이다.
그래서 특정 값이 존재하는지 확인할 때O(1)
의 시간이 걸린다.
배열
에서 contains
는 O(n) 시간 복잡도
를 가짐. 즉, 요소가 많을수록 탐색 시간이 길어짐.
셋
에서 contains
는 O(1) 시간 복잡도
를 가짐. 즉, 요소가 많아져도 탐색 시간은 일정하게 유지 됨.
.
.
.
셋은 내부적으로 해시 테이블을 사용하기 때문에 항목을 찾는 속도가 배열에 비해 훨씬 빠르다.
그래서 contains
메서드를 사용할 때 셋이 배열보다 더 효율적이라고 했던 것이다.
결과적으로 값이 잘 나오고 있다.
오늘 전체적은 계산기 앱의 버튼액션 부분을 구현하면서 몇 가지 중요한 개념과 방법을 알게 됐다.
특히 set을 이용해서 연산자 체크도 했고 시간복잡도에 대한 이해까지 잡아볼 수 있었다.
사실 만들기 너무 급급했는데 가독성도 고려하면서 구현하려고 노력했지만 티가 날지 모르겠다..
일단 완성은 했는데 class별로 나눌 수 있을지 전체적으로 체크를 한번 해보려한다.
그리고 파일을 나눠서 하는게 아직 나에게 너무 어렵다 ..
같은 한 파일에서 클래스를 나누는 것 까진 그나마 할 수 있을 것 같은데
아예 새로운 스위프트파일을 하나 추가해서 따로따로 나누면 도전 할 때마다 실행이 되지않아서
내가 아직 덜 배워서 그런건가 하며 나중에 배우겠거니 하고 미뤘었다.
우선 과제는 마무리했지만, 되도록 파일분리에 도전은 해보고 안되면.. 그때 제출 해야겠다.
아 저도 파일을 도대체 어떻게 나눌까 고민하고 있었는데 반갑네요..!!! ㅋㅋ