Danny의 iOS 컨닝페이퍼
article thumbnail

[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입니다.

좌: 로직 X ---------- 우: 로직 O

 

이렇게 기본적으로 키보드 높이만큼 화면을 움직이는 방법을 알아봤습니다.

 

 

그런데 보면 여기서 또 문제가 발생하네요.

일단 위에서 만든 동작은 키보드가 나타날 때, 무조건 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 를 참고해 주세요.

 

혹시, 더 간편한 방법이 있다면 정보 공유 부탁드립니다. :)

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!