호댕의 iOS 개발

[iOS] 키보드가 컨텐츠를 가리지 않게 하는 법 본문

Software Engineering/iOS

[iOS] 키보드가 컨텐츠를 가리지 않게 하는 법

호르댕댕댕 2022. 2. 12. 13:08

프로젝트를 진행하다보면 종종 키보드를 활성화했을 때 키보드 때문에 뒤에 있는 컨텐츠의 일부가 아예 보이지 않는 문제가 발생하곤 한다. 

화면 위에 키보드가 활성화되어 아무리 뒤에 있는 View를 스크롤 하더라도 보이지 않는다... 😱

이는 사용자가 자신이 작성한 내용을 확인할 수 없기 때문에 사용자 경험을 매우 안 좋게 하는 요인이라고 생각한다. 

그렇다면 이럴 땐 어떻게 해야할까?

 

일단 이 때는 키보드의 높이만큼 컨텐츠가 위로 올라가야 하지 않을까? 라고 생각하고 방법을 찾아보았다. 

 

그 전에 키보드가 활성화되었고, 키보드가 비활성화되는지 알 수 있는 방법이 필요했다. 

이는 이미 만들어져 있는 Notification Center를 사용하면 해결됐다. 

 

내가 사용한 것은 UIResponder에 있는 타입 프로퍼티인 keyboardWillShowNotification, keyboardWillHideNotification이었다. 

이 둘 모두 키보드가 사라지거나 나타나기 직전에 포스트를 해주어 이를 알 수 있도록 해주었다. 

NotificationCenter.default.addObserver(
        self,
        selector: #selector(keyboardWillShow),
        name: UIResponder.keyboardWillShowNotification,
        object: nil
)
NotificationCenter.default.addObserver(
        self,
        selector: #selector(keyboardWillHide),
        name: UIResponder.keyboardWillHideNotification,
        object: nil
)

이렇게 해서 해당 포스트가 오면 selector에 작성한 keyboardWillShow. keyboardWillHide 메서드가 호출될 수 있도록 했다. 

 

그렇다면 어떻게 컨텐츠를 키보드의 높이만큼 올릴 수 있을까?

 

 

1️⃣ 가장 처음에 시도했던 방법은 view.frame.origin.y 자체를 키보드의 높이만큼 올리는 방법이었다. 

하지만 이렇게 할 경우 origin이 바뀌어서 원래 (0, 0)에 있었던 화면을 보지 못하는, 즉 키보드의 높이만큼 위에 화면을 볼 수 없는 문제가 있었다. 

 

그래서 이 방법은 탈락... 

 

2️⃣ 다음에 시도한 방법은 키보드의 높이만큼 bottom에 contentInset을 주는 방법이었다. 

@objc private func keyboardWillShow(_ sender: Notification) {
    if let keyboardFrame = sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey]
        as? NSValue {
        let keyboardRectangle = keyboardFrame.cgRectValue
        let keyboardHeight = keyboardRectangle.height
        scrollView.contentInset.bottom = keyboardHeight
    }
}

scrollView의 contentInset의 경우 get set 프로퍼티로 직접 safe area나 scroll view의 edge와 content view 간에 거리를 줄 수 있었다. 

키보드 만큼의 공간을 둬서 컨텐츠가 가리지 않도록 하는 것이다. 

 

그럼 여기서 키보드의 높이를 어떻게 알 수 있을까?

Notification의 경우 userInfo를 통해 관련된 값과 객체를 딕셔너리 형태로 저장해놓는다. 

여기서 UIResponder.keyboardFrameEndUserInfoKey를 통해 검색을 하게 된다. 

 

keyboardFrameEndUserInfoKey는 애니메이션이 끝날 때 키보드의 프레임을 검색하는 user info key이다. 

이를 통해 키보드의 프레임을 찾고 이 값은 CGRect을 포함하는 NSValue 객체이다. 

 

따라서 일단 NSValue로 타입 캐스팅을 해주고 NSValue의 프로퍼티인 cgRectValue를 통해 Core Graphics의 사각형 구조로 값을 반환해주고 height을 찾는 방법을 선택했다. 

 

 

하지만 contentInset의 경우 스크롤 뷰의 프로퍼티이기 때문에 스크롤 뷰가 아닌 SplitView의 경우 해결할 수 없는 문제가 있었다. 물론 SplitView에 있는 2개의 View 각각 contentInset을 주는 방법도 있겠지만 거의 유사한 코드를 반복적으로 작성해야 하고 만약 hierarchy의 depth가 많아질 경우 이 코드를 모든 ViewController에 적어줘야 하는 문제가 있었다...

 

그래서 생각한 다음 방법!! 

 

3️⃣ window의 프레임에 Inset을 주고 이를 view의 frame과 동일하게 하는 것이다. 

쉽게 말하면 키보드 만큼 화면을 자르는 방법이다. 

@objc private func keyboardWillShow(_ sender: Notification) {
    guard let keyboardFrame = sender.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
          let window = view.window else { return }

    let keyboardRect = keyboardFrame.cgRectValue
    let inset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardRect.height, right: 0)
    self.view.frame = window.frame.inset(by: inset)
}

@objc private func keyboardWillHide(_ sender: Notification) {
    guard let window = view.window else { return }
    view.frame = window.frame
}

하지만 이렇게 하면

외부 키보드를 연결할 때 자동완성이 밑에 뜨면서 옆 부분에는 view가 없어 까맣게 보이는 문제가 있었다. 

이는 window의 Background color를 따로 줘서 해결했다. 

 

⚠️ 다만 여기서 주의할 점!!

 

window가 viewDidLoad 이후에 생성이 되기 때문에 viewDidLoad 내부에 background를 바꾸는 코드를 적어놓으면 window가 nil이기 때문에 적용이 안되는 문제가 있다! 

 

따라서 해당 코드는 sceneDelegate의

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) 메서드에 넣어놓았다. 

Comments