Swift 키보드가 화면을 가릴 때, 뷰를 키보드 위쪽으로 이동시키는 방법(NotificationCenter)

이경은·2024년 1월 19일
0

회원가입 화면을 구현하는 중에 아래쪽에 위치한 textField가 키보드에 가려지는 문제가 있었고, 해당내용을 Notification을 통해 해결한 과정을 작성하였습니다.

해결 순서

  1. 키보드가 올라가거나, 내려가는 이벤트를 감지해서
  2. view를 올려주거나, 내려주는데
  3. 어떤 높이의 textField가 선택되었는지 확인하고
  4. 키보드에 가려질 경우, view를 올려준다.



1.키보드가 올라가거나, 내려갔는가?

우선 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 매개변수에는 키보드가 나타날 때와 사라질 때 호출되는 메서드인 keyboardWillShowkeyboardWillHide를 지정합니다.

  • Notification의 name으로는 UiResponder.keyboarWillShowNotificationUIResponder.keyboardWillHideNotification을 사용하여 각각 키보드가 나타날 때와 사라질 때의 Notification을 등록합니다.

  • 마지막으로 object 매개변수는 특정 객체(위에서는 nil)에서 발생한 Notification만 수신하도록 필터링 할 수 있습니다.



2. 키보드가 올라가거나, 내려갔다면 무엇을 할 것인가?

위의 setUpKeyboard()에서 아직 선언되지 않은 keyboardWillShowkeyboardWillHide는 각각 키보드가 호출되었을 때, 사라질 때 실행되는 메서드로

@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를 위 쪽으로 이동시켜주도록 하겠습니다.



3. 사용자는 무엇을 선택했는가?

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 확장을 사용하면, 특정 시점에 어떤 뷰나 컨트롤러가 현재 응답자로 설정되어 있는지 알 수 있습니다. 이는 주로 키보드가 표시될 때 화면의 특정 부분을 조정하는 등의 작업에서 유용하게 사용될 수 있습니다.



4. 사용자가 선택한 textField가 키보드에 가려질 때, view를 이동시킨다.

이제 키보드의 노출 여부(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가 더 좋겠더라.. 는 생각입니다.

해결에 참고했던 포스팅

0개의 댓글