오늘의 목표
idField 에서 사용자가 아이디를 입력하고 return 키를 눌렀을 때 비밀번호 필드로 이동,
PasswordField에서 return키 눌렀을 때 로그인 시도 하게끔 만드는 걸 해보자.
사용자의 키보드 Return Key 타이틀 설정하기
시뮬레이터에서 키보드 표시하는 단축키 (command k)를 눌러서 어떤 형식의 키보드가 표시되는지 확인할 수 있다.
사용자의 키보드에 Return키에 표시되는 텍스트(타이틀)를 바꾸는 방법
스토리 보드에서 해당 textField를 선택 후 attribute inspector - Text Input Traits - Return Key
아이디 필드에서는 넥스트,
비밀번호 필드에서는 Go , send, Done 다 가능한데 Done으로 하였음
리턴 키에 원하는 타이틀을 넣는 것은 안되고 보이는 옵션들 안에서 선택해야한다.
시스템 언어에 맞게 영어가 아닌 언어가 나올 수 있다고 한다.
UITextField의 Delegate 레퍼런스 확인
원하는 메서드를 찾기 위해서 레퍼런스를 확인한다.
1. 클래스 이름 확인하기
라이브러리 창에서 (shift, command, L) 회색글씨로 클래스 이름을 확인할 수 있다.
2. UITextField 검색 - UITextFieldDelegate
DataSource 검색 - 텍스트 필드는 사용자가 입력하는 거라 데이터 소스가 필요 없다.
Delegate 검색, Topics 의 UITextFieldDelegate를 선택
Overview(개요)를 통해 해당 프로토콜이 어떻게 작동하는지 확인할 수 있다.
https://developer.apple.com/documentation/uikit/uitextfielddelegate
UITextFieldDelegate | Apple Developer Documentation
A set of optional methods to manage editing and validating text in a text field object.
developer.apple.com
Overview 설명 - UITextFieldDelegate
1. 사용자가 textfield를 터치하면 textfield는 delegate 한테 물어본다. 편집을 시작해도돼?
이 때 호출되는 메서드가 textFieldShouldBeginEditing(_:) , 메서드 이름에 should가 들어가면 보통 리턴 타입이 Bool이라고 한다.
true를 리턴하면 다음 동작(2번과정)으로 이동하고 false를 리턴하면 그대로 작업을 끝낸다.
2. first responder 는 이벤트를 가장 먼저 처리할 수 있는 객체, 가장 먼저 처리하는 뷰
키보드가 표시된 상태로 입력하면 입력한 내용이 first responder로 전달 된다.
3. textfield는 델리게이트에게 입력이 시작되었다고 알려주게 됨.
textFieldDidBeginEditing(_:)이 호출되게 된다.
이름에 Did 나 will이 들어가있는 메서드는 이벤트가 시작되었다는 것만 알려주기 때문에 should가 들어가는 메서드 처럼 리턴타입이 있는 것은 아니다.
4. 사용자가 다양한 방식으로 편집하면 textfield 가 delegate에게 또 알려준다.
- 텍스트가 바뀌면 textField(_:shouldchangeCharactersIn:replacementString:)이 호출되고 true를 리턴하면 사용자가 바꾼대로 수정, false를 리턴하면 편집을 취소하고 이전 상태로 돌아간다.
- 사용자가 지우기 버튼을 탭하면 textFieldShouldClear(_:)메서드가 호출되고 true를 리턴하면 사용자가 입력한 값이 전체 삭제된다.
- 리턴키를 누르면 textFieldShouldReturn(_:) 메서드가 호출되고 true를 리턴하면 다음필드로 이동하거나 로그인을 하도록 구현하면 된다.
5. textFieldShouldEndEditing(_:) 메서드가 호출되고 true를 리턴하면 편집이 끝난다.
6. 편집이 끝나면 first responder의 역할을 끝내고 키보드가 사라진다.
7. textFieldDidEndEditing(_:) 메서드가 호출되고 편집사이클이 끝난다.
textFieldShouldReturn(_:) 구현
리턴 키를 눌렀을 때 호출되는 textFieldShouldReturn(_:) 메서드를 통해서 수정할 수 있을 것 같다.
1. textField를 delegate 연결하기
이렇게 연결해야지만 텍스트 필드에서 이벤트가 발생하면 뷰 컨트롤러에 있는 메서드를 호출할 것이다.
그런데 지금 텍스트필드 두개 모두 뷰 컨트롤러와 연결했다.
텍스트필드 여러개를 동일한 뷰컨트롤러를 델리게이트로 지정해도 아무런 문제가 없다고 한다.
2. extension을 통해 UITextFieldDelegate 기능 추가
1. 메서드가 언제 호출되는지 확인하기
익스텐션을 활용해서 UITextFieldDelegate의 기능에 textFieldShouldReturn 메서드를 추가했고
print(#function)을 이용해서 언제 출력되는지 확인했더니
IdField에서 키보드로 return키를 누르니 출력되어 해당 메서드가 출력되는 것을 확인할 수 있었다.
프린트로 뷰를 출력하면 클래스 이름, 메모리주소, 중요한 속성들의 값을 함께 보여준다.
출력되는 내용을 통해서 placeholder 는 아이디 필드라는 것을 알 수 있다.
시뮬레이터의 비밀번호 필드에서 return키를 눌렀을 때도 똑같았다.
여기서 false를 입력하는 이유는 리턴키를 눌렀을 때 텍스트 필드의 편집 상태를 종료하지 않기 위해서이다.
return 키의 기본 동작인 '편집상태 종료 및 키보드 사라짐'을 막고 직접적인 동작을 처리할 수 있다.
2-1. Field 별 코드 추가하는 방법 / 비추천
그러면 이제 textFieldShouldReturn을 이용해서 첫번째 파라미터가 idField라면 passwordField로 이동하게 하면 되고 파라미터가 passwordField라면 로그인 시도하게 하면 된다.
첫번째 파라미터가 어떤 필드인지는 어떻게 구분할까?
placeholder로 구분가능하지 않을까?
extension ViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
print(#function, textField)
if textField.placeholder == "아이디를 입력해 주세요"{
}
return false
}
}
이렇게 입력하면 텍스트필드의 placeholder가 "아이디를 입력해 주세요" 가 되면 idField를 나타내는 것이니 구분이 가능할 수 있다.
하지만 "아이디를 입력해주세요"의 문자열이 수정될 수도 있고, 다른 언어로 번역될 수도 있기 때문에
이런 방식은 옳지 않다.
2-2. Field 별 코드 추가하는 방법
ViewController 안 클래스에
@IBOutlet weak var idField: UITextField!
@IBOutlet weak var passwordField: UITextField!
이렇게 idField, passwordField를 저장한 것을 볼 수 있다.
이 값은 비교할 때도 사용 가능하다.
switch 문을 사용하여 각 field 별로 return키를 눌렀을 때 값을 입력해주면 된다.
extension ViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
print(#function, textField)
switch textField {
case idField:
passwordField.becomeFirstResponder()
case passwordField:
login(self)
default:
break
}
return false
}
}
switch 문 코드 분석
textField가 idField의 경우
passwordField를 firstResponder 로 만든다.
textField가 passwordField의 경우
login 메서드를 실행하고 있다.
기본 값으로 실행중지를 설정하였다.
2-3. login (self) 메서드
login 메서드는 어디서 갑자기 나왔냐면 예전에 아래와 같이 ViewController Class 안에 액션과 관련된 코드를 이미 적어 놓았다.
@IBAction func login(_ sender: Any) {
guard let id = idField.text, !id.isEmpty else {
showAlert(message: "아이디를 입력해 주세요")
return
}
guard let password = passwordField.text, !password.isEmpty else {
showAlert(message: "비밀번호를 입력해 주세요")
return
}
if id == "kxcoding" && password == "1234" {
resultLabel.text = "로그인 성공"
} else {
resultLabel.text = "로그인 실패"
}
}
그렇다면 이걸 그대로 복사해와도 되지만 코드가 중복되니까 더 간단하게 불러올 수 있는 방법이 있다.
login이라는 액션의 이름을 적고 ()괄호 안에 self를 입력하면 된다. (함수 호출)
그럼 왜 self라고 적었을까?
파라미터로 원래 sender를 전달해야되는데 지금 어떤 걸 전달해야 될지 모르는 상황에서는 self 라고 적어도 된다.
(_ sender: Any): 액션을 트리거한 객체(버튼, 텍스트필드 등)를 매개변수로 받는다는 말이고 Any 타입이라서 어떤 객체든 받을 수 있다는 말이다.
self는 현재 뷰 컨트롤러 객체이므로 무난하게 전달할 수 있고 self는 항상 접근 가능한 객체라 nil이 될 위험이 없다.
여기서 self는 ViewController 자신을 가르키는 말, 메서드 내부에서 자신을 명시적으로 가리킬 때 사용한다.
오늘의 목표
아이디의 길이에 제한을 둔다.
6~12자로 길이를 제한하고 아이디를 입력한 후 이 길이를 확인하고 입력한 값의 범위에서 벗어나면 편집을 유지, passwordField로 못넘어가게 한다.
그리고 빨간색으로 테두리를 바꾸는 것도 구현하자.
textFieldShouldEndEditing
이건 편집을 끝내기 직전에 실행되는 메서드이다.
여기서 false를 리턴하면 계속해서 편집상태가 유지되고 키보드도 사라지지 않는다.
아이디의 길이가 6~12글자 범위에 속하지 않을때 편집을 유지하게 하는 코드를 작성해준다.
extension ViewController: UITextFieldDelegate {
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
if textField == idField { //편집중인 텍스트필드가 아이디필드인지 확인
let cnt = textField.text?.count ?? 0 //값이 nil이면 기본값으로 0을 사용
let isValidId = (6 ... 12).contains(cnt) //cnt가 6이상 12이하인지 검사, 그 사이면 true
return isValidId //isValid가 true면 true 그대로 전달
}
return true //idField를 제외한 필드는 편집종료 허용
}
}
보더 컬러를 빨간색으로 지정하는 방법
보더는 테두리를 말하는 것이다. 보더 컬러는 자동으로 할 수 없고 직접 입력해야한다.
extension ViewController: UITextFieldDelegate {
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
if textField == idField {
let cnt = textField.text?.count ?? 0
let isValidId = (6 ... 12).contains(cnt)
textField.layer.borderWidth = isValidId ? 0 : 1
// isValidId가 true면 border의 두께가 0, false면 두께 1
textField.layer.borderColor = isValidId ? nil : UIColor.red.cgColor
// isValidId가 true면 border의 색이 지정색없음, false면 UIColor.red로 지정
textField.layer.cornerRadius = isValidId ? 0 : 5
// isValidId가 true면 모서리 둥글기 없음, false면 5(약간둥글게)
textField.tintColor = isValidId ? view.tintColor : .red
// isValidId가 true면 뷰의 기본색상으로, false면 빨간색 커서로
return isValidId
}
return true
}
먼저 레이어 속성에 접근한 다음에 borderWidth로 두께를 직접 지정하고
그 다음 레이어에 다시 접근해서 borderColor를 설정하면 된다.
근데 borderColor는 UIColor 가 아니라 CGColor다.
CGColor 는 코어 그래픽스 컬러인데 UIColor가 제공하는 속성으로 쉽게 설정할 수 있다. UIColor.색상.cgColor로 입력하면 된다.
필드의 모양을 보면 라운드 처리가 되어 있다.
입력포커스(커서, tintColor)도 같은 색으로 바꿔준다.
매번 편집이 끝나기 전에 이 메서드가 호출되니까 idField의 길이가 6~12 사이에 있다면 기본 컬러로 복구되는 모습을 볼 수 있다.
오늘의 목표
기본적인 로그인 버튼은 비활성화로 설정하고
아이디와 비밀번호를 모두 입력했을 때만 활성화되도록 변경하기
텍스트 필드에서 편집을 할 때마다 입력 길이를 확인한 다음에 버튼의 상태를 바꾸는 것
textField(_:shouldChangeCharactersIn:replacementString:)
이번에 필요한 메서드는 textField(_:shouldChangeCharactersIn:replacementString:) 이다
레퍼런스 살펴보기
optional func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool
첫번째 파라미터는 이 메서드를 호출한 텍스트필드가 전달되고
두번째 파라미터로 입력 후 바뀌는 범위가 전달 ,
세번째 파라미터로 새롭게 바뀌는 텍스트가 전달 된다.
키보드에서 입력할 때는 하나의 문자열이 저장되어 있는데 붙여넣기를 통한 문자열 입력도 가능.
삭제할 때는 빈 문자열이 저장되어 있다.
세번째파라미터의 길이를 확인해서 삭제인지 입력인지 확인할 수 있다.
길이가 0이면 삭제 나머지는 입력으로 이해할 수 있다.
true를 리턴하면 입력한 값이 그대로 반영되고 false는 편집한 내용을 버리고 이전 텍스트를 그대로 유지한다.
키보드를 입력하거나 삭제할 때만 호출되는게 아니라 사용자가 잘라넣기 붙여넣기 등을 사용해서 텍스트가 바뀌게 되면 호출된다.
언제 호출되는 메서드일까?
사용자가 핸드폰 화면에 w라는 키보드를 터치했다고 가정하면
터치함과 동시에 이 메서드가 호출된다.
w를 입력했다는 그 값을 가지고 있다.
그래서 true를 리턴하면 화면에 w라는 글자가 보이게 되는 것이고
false를 리턴하면 아무것도 입력되지 않는다.
되게 짧은 몇 밀리초안에 수행되는 메서드라서 사용자 입장에서는 딜레이를 전혀 느끼지 않는 것이다.
이 메서드는 수정 직전에 호출되는 메서드이기 때문에 갖고 있는 문자열의 길이가 실제 입력된 문자열과 차이가 생긴다.
이는 삭제할때에도 마찬가지다.
true가 리턴되기 전의 값을 가지고 있기 때문에 최종 텍스트의 길이를 파악하기 어렵다는 한계가 있을 수 있다.
그럼 replacingCharacters 메서드를 통해 최종 텍스트의 길이를 파악하는 메서드를 추가해야한다.
코드 작성
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text, let range = Range(range, in: text) else { return true }
let finalText = text.replacingCharacters(in: range, with: string)
return true
}
}
shouldChangeCharactersIn 에서 쓰이는 Range는 NS Range 타입이다.
objective C 부터 사용된 타입이고
replacingCharacters메서드에 쓰이는 range 는 Range Expression으로 swift에서 도입된 새로운 Range 타입이 와야한다.
타입이 맞지 않기 때문에 바로 올 수 없기 때문에 바꿔줘야한다.
그리고 textField도 optional이기 때문에 바인딩 해야한다.
그래서 guard let으로 textField를 바인딩 하면서 Range도 바꾸어 주었다.
이제 최종 텍스트의 길이까지 파악할 수 있게 되었으니
그 길이를 확인해서 로그인 버튼 활성화/비활성화를 구현하는 코드를 짜야한다.
UIButton 아웃렛으로 연결
그 전에
로그인 button이 아웃렛으로 연결되어 있는지 확인해서 없으면 아웃렛으로 연결해줘야한다.
loginButton이라는 이름으로 아웃렛에 연결된 모습이다.
아웃렛으로 연결된 UIButton을 활성화 하는 조건을 추가해준다.
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text, let range = Range(range, in: text) else {
return true
}
let finalText = text.replacingCharacters(in: range, with: string)
loginButton.isEnabled = !finalText.isEmpty
return true
}
}
loginButton.isEnabled - 트루를 저장하면 버튼이 활성화 되고 false를 저장하면 버튼이 비활성화 된다.
첫 화면 구동했을 때 로그인 버튼을 비활성화 시키는 방법
스토리 보드 - UIButton 선택 - attribute inspector - Control
여기서 State 에 Enabled 를 체크 해제 하면
처음 화면이 실행 될 때 버튼이 비활성화 된다.
챌린지
idField만 입력해도 로그인 버튼이 활성화 된다.
idField와 passwordField 버튼 둘 다 입력해야지만 로그인 버튼이 활성화 되게끔 설정해야한다.
내 답변
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text, let range = Range(range, in: text) else {
return true
}
let finalText = text.replacingCharacters(in: range, with: string)
let idText = idField.text ?? ""
let passwordText = passwordField.text ?? ""
loginButton.isEnabled = !idText.isEmpty && !passwordText.isEmpty
return true
}
}
선생님 답
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var finalId = idField.text ?? ""
var finalPassword = passwordField.text ?? ""
if textField == idField {
guard let range = Range(range, in: finalId) else {
return true
}
finalId = finalId.replacingCharacters(in: range, with: string)
} else if textField == passwordField {
guard let range = Range(range, in: finalPassword) else {
return true
}
finalPassword = finalPassword.replacingCharacters(in: range, with: string)
}
loginButton.isEnabled = !finalId.isEmpty && !finalPassword.isEmpty
return true
}
}
선생님은 idField와 passwordField를 선언해주고
그에 맞게 코드를 변경하였다.
내 코드도 시뮬레이터 돌렸을 때 문제없이 발생하고 있었다.
선생님 코드 처럼 복잡하게 구현해야하는 이유가 뭘까..?
문제의 핵심
- 문제는 idField와 passwordField의 텍스트 값이 모두 채워져야만 로그인 버튼이 활성화되도록 하는 것입니다.
- 문제의 의도는 두 필드가 모두 입력되었을 때만 로그인 버튼이 활성화되도록 만들라는 것이기 때문에, 각 필드의 변경된 상태를 개별적으로 확인하는 것이 중요합니다.
왜 선생님 코드가 더 좋은지?
선생님 코드에서는 idField와 passwordField의 변경을 각각 추적하고 그에 맞게 최종 값을 구한 후 loginButton의 활성화 여부를 결정합니다.
중요한 부분
- idField와 passwordField 각각에 대해 입력이 일어날 때마다, 해당 텍스트 필드에 입력된 내용이 정확하게 반영됩니다.
- 예를 들어, idField에서 문자를 입력하거나 삭제할 때, idField만 업데이트되고 passwordField는 그대로 유지됩니다.
- 마찬가지로 passwordField에서 입력이 있을 때, passwordField만 업데이트되고 idField는 영향을 받지 않습니다.
이 방식은 각각의 텍스트 필드의 상태를 정확하게 추적할 수 있기 때문에, 두 필드의 텍스트가 모두 채워졌을 때만 버튼을 활성화하는 로직을 정확히 처리할 수 있습니다.
반면, 내 코드에서의 문제점
내 코드에서는 shouldChangeCharactersIn 메서드에서 idField와 passwordField의 상태를 모두 한 번에 업데이트하려고 했습니다.
- 여기서 idField.text와 passwordField.text를 그대로 사용해서 상태를 비교하고 있죠.
- 이 방식은 textField(_:shouldChangeCharactersIn:replacementString:) 메서드 호출 직전에 변경된 값을 반영하지 않고, 수정된 텍스트 필드를 기준으로 한 텍스트 값만 확인하는 문제를 가질 수 있습니다.
따라서 idField나 passwordField의 텍스트가 실시간으로 업데이트되기 전에 loginButton.isEnabled 상태를 결정하는 것이 아니라, 실제로 텍스트가 업데이트된 변경된 상태를 반영하려면 각각의 필드에 대한 직접적인 처리가 필요합니다.
선생님 코드와 비교한 이유:
선생님 코드에서는 각 필드의 텍스트를 독립적으로 처리하고 있기 때문에, idField와 passwordField의 각각의 텍스트가 변경될 때마다 버튼 활성화 여부를 정확하게 판단할 수 있습니다.
결론
- 내 코드도 동작은 하지만, 변경된 값을 즉시 반영하는 방식을 따르지 않아서 정확하게 실시간 텍스트를 반영하는 방식으로 선생님처럼 작성하는 것이 더 적합합니다.
- 선생님 코드가 더 명확하고 올바른 방법이에요.
'IT > 프로그래밍 노트' 카테고리의 다른 글
011 Function Type, Closure Type, 함수자료형, 클로저 자료형 (0) | 2025.03.19 |
---|---|
010 Closure 클로저 (0) | 2025.03.19 |
009 Tuple 튜플 (0) | 2025.03.17 |
008 함수 function (0) | 2025.03.17 |
027-2 Table view, DataSource, Delegate, 디자인 (0) | 2025.03.16 |