[TIL #21] 2023 / 05 / 08
TextField 나 TextView 등 터치 이벤트로 키보드가 올라올 때,
키보드가 텍스트 필드를 가리는 현상이 발생할 때가 있습니다.
이런 이슈를 방지하는 방법을 간단히 알아봅시다.
방법 - 노티피케이션을 이용
NotificationCenter으로 키보드가 보이거나 가려질 때, 이벤트를 받아서 사용하는 방법입니다.
(참고, 아래 메서드는 targets iOS 9.0 이상에서는 굳이 removeObserver를 해줄 필요가 없다고 하네요.)
func setupKeyboardEvent() {
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
setupKeyboardEvent()
}
이제 동작에 대한 정의 해봅시다.
여기서 필요한 동작은 키보드가 올라갈 때, 내려갈 때마다 뷰의 위치를 옮겨주면 되겠죠?
자, 이렇게 직접 원하는 값을 넣어줄 수 있습니다.
@objc func keyboardWillShow(_ sender: Notification) {
view.frame.origin.y -= 300
}
@objc func keyboardWillHide(_ sender: Notification) {
view.frame.origin.y = 0
}
그런데 이렇게 하드코딩을 하게 되면, 우아한 코드가 아니겠죠?
그리고 또 기기의 기종마다 키보드의 높이가 다르기 때문에, 문제가 발생할 수 있습니다.
그러므로 사용하고 있는 키보드의 높이를 구해서 넣어봅시다.
@objc func keyboardWillShow(_ sender: Notification) {
// 현재 동작하고 있는 이벤트에서 키보드의 frame을 받아옴
guard let keyboardFrame = sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardHeight = keyboardFrame.cgRectValue.height
// ⭐️ 이 조건을 넣어주지 않으면, 각각의 텍스트필드마다 keyboardWillShow 동작이 실행되므로 아래와 같은 현상이 발생
if view.frame.origin.y == 0 {
view.frame.origin.y -= keyboardHeight
}
}
@objc func keyboardWillHide(_ sender: Notification) {
if view.frame.origin.y != 0 {
view.frame.origin.y = 0
}
}
위와 같은 로직을 넣어주지 않으면, 각 텍스트 필드를 클릭할 때마다 keyboardWillShow 동작을 실행하기 때문에,
각 텍스트 필드를 클릭할 때마다 아래 GIF처럼 화면이 계속 올라가게 됩니다. (여러 번 화면이 올라가게 됩니다.)
첫번째 텍스트필트 클릭 후, 연속으로 두번 째 텍스트필드를 클릭하는 GIF입니다.
이렇게 기본적으로 키보드 높이만큼 화면을 움직이는 방법을 알아봤습니다.
그런데 보면 여기서 또 문제가 발생하네요.
일단 위에서 만든 동작은 키보드가 나타날 때, 무조건 view를 위로 이동 시켜주는 동작입니다.
그렇기 때문에 첫 번째 텍스트필드를 선택해도 화면이 올라가게 되므로, 현재 입력중인 첫 번째 텍스트 필드가 가려지네요.
어떻게 해야 두 번째 텍스트 필드에서만 동작하게 할 수 있을까요?
여긴 조금 복잡한데...
UIResponder로 현재 반응하고 있는 텍스트 필드를 알아낸 다음, 그 위치값을 갖고 계산을 해줘야 합니다.
먼저 이와 같이 UIResponder를 익스텐션으로 만들어 줍시다.
자세한 설명은... 패스...
// 현재 응답받는 UI를 알아내기 위해 사용 (textfield, textview 등)
extension UIResponder {
private struct Static {
static weak var responder: UIResponder?
}
static var currentResponder: UIResponder? {
Static.responder = nil
UIApplication.shared.sendAction(#selector(UIResponder._trap), to: nil, from: nil, for: nil)
return Static.responder
}
@objc private func _trap() {
Static.responder = self
}
}
그리고 키보드가 올라올 때, 동작(keyboardWillShow)을 다시 정의해 봅시다.
(살짝 복잡하니 설명은 주석을 참고해 주세요.)
@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 }
// Y축으로 키보드의 상단 위치
let keyboardTopY = keyboardFrame.cgRectValue.origin.y
// 현재 선택한 텍스트 필드의 Frame 값
let convertedTextFieldFrame = view.convert(currentTextField.frame,
from: currentTextField.superview)
// Y축으로 현재 텍스트 필드의 하단 위치
let textFieldBottomY = convertedTextFieldFrame.origin.y + convertedTextFieldFrame.size.height
// Y축으로 텍스트필드 하단 위치가 키보드 상단 위치보다 클 때 (즉, 텍스트필드가 키보드에 가려질 때가 되겠죠!)
if textFieldBottomY > keyboardTopY {
let textFieldTopY = convertedTextFieldFrame.origin.y
// 노가다를 통해서 모든 기종에 적절한 크기를 설정함.
let newFrame = textFieldTopY - keyboardTopY/1.6
view.frame.origin.y -= newFrame
}
}
여기서 중요한 부분은 textFieldBottomY > keyboardTopY 이 부분입니다.
키보드가 텍스트 필드를 가릴 때, 화면을 올려주는 로직으로 가장 중요합니다.
그리고 화면이 올라가는 높이는 알아서 보기 좋게 설정해 주면 됩니다.
(Keyboard Design GuideLine 도 있으니 참고해서 만드는 걸 추천드립니다.)
이렇게 두 번째 텍스트필드(키보드가 가리는 텍스트필드)에서만 화면이 올라가는 동작을 만들어 봤습니다.
참고: Swift Arcade
방법 - Protocol로 만들기
마찬가지로 NotificationCenter를 이용합니다.
하지만 여기선 프로토콜을 채택하여 사용하는 방식으로 만들었습니다.
일단 위와 같이 UIResponder를 만들어 줬습니다.
// 현재 응답받는 UI를 알아내기 위해 사용 (textfield, textview 등)
extension UIResponder {
private struct Static {
static weak var responder: UIResponder?
}
static var currentResponder: UIResponder? {
Static.responder = nil
UIApplication.shared.sendAction(#selector(UIResponder._trap), to: nil, from: nil, for: nil)
return Static.responder
}
@objc private func _trap() {
Static.responder = self
}
}
다음으로 KeyboardEvent란 프로토콜을 만들어줬습니다.
(계산 방법은 동일합니다.)
import UIKit
protocol KeyboardEvent where Self: UIViewController {
var transformView: UIView { get }
func setupKeyboardEvent()
}
extension KeyboardEvent where Self: UIViewController {
func setupKeyboardEvent() {
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: OperationQueue.main) { [weak self] notification in
self?.keyboardWillAppear(notification)
}
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification,
object: nil,
queue: OperationQueue.main) { [weak self] notification in
self?.keyboardWillDisappear(notification)
}
}
func removeKeyboardObserver() {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
private func keyboardWillAppear(_ sender: Notification) {
// keyboardFrame: 현재 동작하고 있는 이벤트에서 키보드의 frame을 받아옴
// currentTextField: 현재 응답을 받고있는 UITextField를 알아냅니다.
guard let keyboardFrame = sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let currentTextField = UIResponder.currentResponder as? UITextField else { return }
// Y축으로 키보드의 상단 위치
let keyboardTopY = keyboardFrame.cgRectValue.origin.y
// 현재 선택한 텍스트 필드의 Frame 값
let convertedTextFieldFrame = transformView.convert(currentTextField.frame,
from: currentTextField.superview)
// Y축으로 현재 텍스트 필드의 하단 위치
let textFieldBottomY = convertedTextFieldFrame.origin.y + convertedTextFieldFrame.size.height
// Y축으로 텍스트필드 하단 위치가 키보드 상단 위치보다 클 때 (즉, 텍스트필드가 키보드에 가려질 때가 되겠죠!)
if textFieldBottomY > keyboardTopY {
let textFieldTopY = convertedTextFieldFrame.origin.y
// 노가다를 통해서 모든 기종에 적절한 크기를 설정함.
let newFrame = textFieldTopY - keyboardTopY/1.6
transformView.frame.origin.y -= newFrame
}
}
private func keyboardWillDisappear(_ sender: Notification) {
if transformView.frame.origin.y != 0 {
transformView.frame.origin.y = 0
}
}
}
이제 사용하고자 하는 뷰컨트롤러에서 KeyboardEvent 채택하여 사용하면 됩니다.
// 프로토콜 KeyboardEvent 채택
class ViewController: UIViewController, KeyboardEvent {
// 키보드 이벤트를 받을 때 움직일 뷰를 정해줍니다.
var transformView: UIView { return self.view }
override func viewDidLoad() {
super.viewDidLoad()
// KeyboardEvent의 setupKeyboardEvent
setupKeyboardEvent()
}
// KeyboardEvent에서 사용된 addObserver는 자동으로 제거가 안되므로 여기선 제거해 줍시다.
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// KeyboardEvent의 removeKeyboardObserver
removeKeyboardObserver()
}
}
뷰컨트롤러가 엄청 깔끔해 졌습니다 ㅎㅎ
방법 - 델리게이트 사용
사실 키보드 높이 이런거 상관없이 대략적인 높이만 올려줘도 된다면,
그냥 간단하게 델리게이트에다 박아주는게 가장 간편합니다.
텍스트필드 델리게이트를 이용하여, 입력이 시작될 때와 종료될 때 뷰를 이동시켜주면 끝!
extension ViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField == self.textField {
view.frame.origin.y = 0
} else if textField == self.nextTextField {
// 부드러운 효과를 위해 애니메이션 처리
UIView.animate(withDuration: 0.3) {
let transform = CGAffineTransform(translationX: 0, y: -200)
self.view.transform = transform
}
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
if textField == self.textField {
view.frame.origin.y = 0
} else if textField == self.nextTextField {
UIView.animate(withDuration: 0.3) {
let transform = CGAffineTransform(translationX: 0, y: 0)
self.view.transform = transform
}
}
}
}
방법 - 새로운 방법
이 방법은 iOS 15.0 이상부터 추가된 새로운 키보드 레이아웃 잡는 방법이라고 합니다.
아직 제대로 사용을 안 해봐서 참고링크(예제링크) 만 남기겠습니다.
마무리
전체코드는 GitHub 를 참고해 주세요.
혹시, 더 간편한 방법이 있다면 정보 공유 부탁드립니다. :)
'프로젝트' 카테고리의 다른 글
[Swift/ TIL #23] Realm Cheat Sheet (0) | 2023.05.16 |
---|---|
[Swift/ TIL #22] LocationManager 사용 (0) | 2023.05.11 |
[Swift/TIL #20] 이미지 메모리 최적화 방법들 (WWDC 18) (0) | 2023.05.02 |
[Swift/TIL #19] 현재 입력된 텍스트의 줄 수 구하기 (0) | 2023.04.30 |
[Swift/TIL #18] UICollectionViewCompositionalLayout으로 Cell의 크기를 동적으로 만들어 보자 (더보기 버튼) (0) | 2023.04.28 |