회원가입 화면을 구현하는 중에 아래쪽에 위치한 textField가 키보드에 가려지는 문제가 있었고, 해당내용을 Notification을 통해 해결한 과정을 작성하였습니다.
우선 view를 이동하는 기준은 키보드가 올라왔을 때
또는 내려갔을 때
이므로 우선 키보드가 어떤 상태인지를 알아야할 필요가 있습니다.
이것은 NotificationCenter를 활용해 알 수 있습니다.
func setUpKeyboard() {
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
setUpKeyboard
함수는 키보드가 나타날 때와 사라질 때의 Notification을 등록합니다.
NotificationCenter.default.addObserver
메서드를 사용해서 해당 ViewController에서 Notification을 수신할 수 있도록 설정합니다.
selector
매개변수에는 키보드가 나타날 때와 사라질 때 호출되는 메서드인 keyboardWillShow
와 keyboardWillHide
를 지정합니다.
Notification의 name으로는 UiResponder.keyboarWillShowNotification
과 UIResponder.keyboardWillHideNotification
을 사용하여 각각 키보드가 나타날 때와 사라질 때의 Notification을 등록합니다.
마지막으로 object
매개변수는 특정 객체(위에서는 nil
)에서 발생한 Notification만 수신하도록 필터링 할 수 있습니다.
위의 setUpKeyboard()
에서 아직 선언되지 않은 keyboardWillShow
와 keyboardWillHide
는 각각 키보드가 호출되었을 때, 사라질 때 실행되는 메서드로
@objc func keyboardWillShow() {
// view의 frame을 y축으로 -300만큼 이동(= 위로 이동)
view.frame.origin.y = -300
}
// view의 frame을 y축 = 0으로 이동(= 원위치)
@objc func keyboardWillHide() {
view.frame.origin.y = 0
}
위처럼 임의의 숫자로 지정해 무조건 view를 이동시킨다면, 화면 가장 상단에 있는 textField를 선택했을 때도, 가장 하단의 textField를 선택했을 때도 view가 이동하게 됩니다.
따라서 어떤 조건에서 view를 이동시켜 줄 것인가를 keyboardWillShow
메서드에 추가해야 합니다.
예시에서는 키보드의 높이
> textField의 높이
일 때, view를 위 쪽으로 이동시켜주도록 하겠습니다.
iOS에서는 사용자의 터치나 키보드 입력과 같은 이벤트에 대한 응답을 처리하기 위해 UIResponder 클래스를 사용합니다. UIResponder의 서브클래스에는 특정 이벤트에 응답하는 메서드들이 구현되어 있습니다.
여기서 UIResponder extension은 현재 응답자를 확인하는 용도로 사용됩니다.
// 현재 응답받고 있는 UI를 알아내기 위한 extension
extension UIResponder {
private struct Static {
static weak var responder: UIResponder?
}
// 현재 응답자 반환하는 computed property
static var currentResponder: UIResponder? {
// Static.responder 초기화
Static.responder = nil
// 특정 동작을 유발시켜서 현재 응답자를 찾아내는 방식
UIApplication.shared.sendAction(#selector(UIResponder._trap), to: nil, from: nil, for: nil)
// 찾아낸 UIResponder 반환
return Static.responder
}
// 현재 응답자 저장하는 private 메서드
@objc private func _trap() {
Static.responder = self
}
}
위의 코드를 살펴보면, UIResponder의 extension으로 private 구조체 Static을 사용하여 현재 응답자를 저장하는 역할을 합니다. 이때 weak 키워드를 사용하여 강한 참조 순환을 방지합니다.
Static.responder : 현재 응답자를 저장하는 프로퍼티입니다. weak로 선언되어 있어 강한 참조 순환을 방지합니다.
currentResponder : 현재 응답자를 반환하는 computed property입니다. 이때 UIApplication.shared.sendAction 메서드를 사용하여 _trap() 메서드를 호출하고, 이를 통해 현재 응답자를 찾아냅니다.
_trap() : 현재 응답자를 저장하는 private 메서드로, sendAction 메서드에서 호출되어 현재 응답자를 Static.responder에 저장합니다.
이렇게 구현된 UIResponder 확장을 사용하면, 특정 시점에 어떤 뷰나 컨트롤러가 현재 응답자로 설정되어 있는지 알 수 있습니다. 이는 주로 키보드가 표시될 때 화면의 특정 부분을 조정하는 등의 작업에서 유용하게 사용될 수 있습니다.
이제 키보드의 노출 여부(Notification)와 사용자가 선택한 textField(UIResponder)가 무엇인지 알 수 있게 되었고, '서로의 높이를 기준'으로 view를 이동시키도록 keyboardWillShow
메서드를 수정하겠습니다.
앞서 언급한 것처럼 조건은 키보드의 높이
> textField의 높이
로 지정합니다.
@objc func keyboardWillShow(_ sender: Notification) {
// keyboardFrame : 현재 동작하고 있는 이벤트에서 키보드의 frame을 전달
// currentTextField : UIResponder.currentResponder로부터 현재 응답을 받고 있는 UITextField를 확인
guard let keyboardFrame = sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, let currentTextField = UIResponder.cuurentResponder as? UITextField else { return }
// keyboardYTop : 키보드 상단의 y값
let keyboardYTop = keyboardFrame.cgRectValue.origin.y
// convertedTextFieldFrame : 현재 선택한 textField의 frame을 해당 텍스트 필드의 superview에서 view cooridnate system으로 변환
let convertedTextFieldFrame = view.convert(currentTextField.frame, from : currentTextField.superview)
// textFieldYBottom : 텍스트필드 하단의 y값 = 텍스트필드의 y값(=y축 위치) + 텍스트필드의 높이
let textFieldYBottom = convertedTextFieldFrame.origin.y + convertedTextFieldFrame.size.height
// textField 하단이 키보드 상단보다 높을 때 view의 높이를 조정
if textFieldYBottom > keyboardYTop {
let textFieldYTop = convertedTextFieldFrame.origin.y
let properTextFieldHeight = textFieldYTop - keyboardYTop / 1.3
view.frame.origin.y = -properTextFieldHeight
}
}
그리고 아래와 같이 viewDidLoad
에서 setUpKeyboard
함수를 호출해 키보드 Notification을 등록합니다.
class SignUpViewController : UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
setUpKeyboard()
}
...
}
extension SignUpVC {
// textField 밖을 터치했을 때, 키보드를 감추는 메소드
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
print("touchesBegan")
}
func setUpKeyboard() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
@objc func keyboardWillShow(_ sender: Notification) {
// keyboardFrame : 현재 동작하고 있는 이벤트에서 키보드의 frame을 받아옴
// currentTextField : 현재 응답을 받고 있는 UITextField를 확인한다.
guard let keyboardFrame = sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, let currentTextField = UIResponder.currentResponder as? UITextField else { return }
// keyboardYTop : 키보드 상단의 y값(=높이)
let keyboardYTop = keyboardFrame.cgRectValue.origin.y
// convertedTextFieldFrame : 현재 선택한 textField의 frame값(=CGRect). superview에서 frame으로 convert를 했다는데.. 무슨 말인지..
let convertedTextFieldFrame = view.convert(currentTextField.frame, from: currentTextField.superview)
// textFieldYBottom : 텍스트필드 하단의 y값 = 텍스트필드의 y값(=y축 위치) + 텍스트필드의 높이
let textFieldYBottom = convertedTextFieldFrame.origin.y + convertedTextFieldFrame.size.height
// textField 하단의 y축 값이 키보드 상단의 y축 값보다 클 때(키보드가 textField를 침범할 때)
if textFieldYBottom > keyboardYTop {
let textFieldYTop = convertedTextFieldFrame.origin.y
let properTextFieldHight = textFieldYTop - keyboardYTop/1.3
// view의 위치를 변경
view.frame.origin.y = -properTextFieldHight
}
}
@objc func keyboardWillHide(_ sender: Notification) {
if view.frame.origin.y != 0 {
view.frame.origin.y = 0
}
}
}
구현하고보니 이런 view의 이동이 없는 UI가 더 좋겠더라.. 는 생각입니다.