[iOS] - 키보드가 특정 뷰를 가릴 때 UIScrollView로 뷰를 위로 올리기


간혹 텍스트 입력을 위해 키보드가 필요한 뷰에서 키보드가 올라와 사용자에게 보여야 할 뷰가 가려질 때가 있다. UIScrollView를 사용해서 이런 현상을 어떻게 해결할 지 보자.


image

위의 화면과 같이 정중앙에 UITextView를 만들었다. 텍스트 뷰에 뭐를 입력하려고 텍스트 뷰 안을 터치하면 아래와 같이 키보드가 올라온다.

image

화면을 보면 키보드의 윗부분이 텍스트 뷰를 가리게 된다. 아래와 같이 키보드가 올라와 있지 않을 때는 텍스트 뷰가 원래의 위치(화면 중앙)에 있고, 키보드가 올라왔을 때는 키보드 위로 텍스트 뷰를 올리고 싶으면 어떻게 해야 할까?

image

키보드가 올라왔을 때 뷰의 위치를 조정하는 방법에는 여러 방법이 있을 수 있다. 이런 상황에서는 UIScrollView를 사용하는 것이 가장 간단하다.

심지어 애플은 Apple Developer Doucment에 이런 상황을 어떻게 처리할 지에 대한 코드도 남겨놓았다. 이런 것에 대한 공식 문서가 있을 정도로 이런 케이스가 많다는 걸 의미할 것이다.

“Moving Content That Is Located Under the Keyboard” 부분을 보면 키보드가 올라왔을 때 앱 컨텐츠의 일부를 가리는 현상과 그것을 해결하는 방법에 대해 나와있다. 키보드가 컨텐츠를 가리게 되면 이 컨텐츠를 키보드 위로 올려야 한다. 키보드와 UITextView와 같은 텍스트를 다루는 객체를 관리하는 가장 간단한 방법은 위에서도 잠깐 언급했지만 UITableView와 같은 UIScrollView 객체를 사용하는 것이 가장 간단하다.

키보드 위로 뷰 올리기

그러면 UIScrollView를 사용해서 키보드 위로 컨텐츠를 어떻게 올릴까?

  1. 키보드 사이즈를 구한다.
  2. UIScrollView의 하단 content inset을 키보드의 높이와 같게 한다.
  3. 필요하다면 스크롤 뷰를 스크롤한다.

0. 스크롤 뷰 생성

image

만들어 둔 이 화면에서 시작해보자. 먼저 UIScrollView가 화면을 꽉 채우게 하고, 안에 화면 사이즈만한 컨텐츠 뷰를 만든다음, 컨텐츠 뷰에 텍스트 뷰를 넣을 것이다.

뷰 계층을 보면 UIScrollView 안에 UIView가 있고, UIView에 UITextView가 아래와 같이 있다.

image image

이러면 UIView는 UIScrollView의 크기와 같기 때문에 스크롤이 활성화되어 있지 않다.

image

키보드를 올렸을 때 아래와 같이 키보드가 텍스트 뷰 밑을 가리고, 스크롤은 여전히 되지 않는다.

image

1. 키보드 사이즈 구하기

먼저 스크롤 뷰를 관리하는 뷰 컨트롤러 등에서 키보드가 화면에 올라오고, 다시 해제되는 것에 대한 notification을 다룰 handler method를 구현한다.

private func registerForKeyboardNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWasShown(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)
    }
    
    @objc private func keyboardWasShown(_ notification: Notification) {
        let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
        
        if let keyboardSize = keyboardFrame?.cgRectValue {
            // 여기에서 keyboardSize.height로 키보드 높이에 접근할 수 있다.
        }
    }

registerForKeyboardNotifications() method에서 키보드가 완전히 올라왔을 때를 알 수 있게 observer를 등록했다. 참고로 키보드는 아래에서 위로 올라오고, 이동안 키보드의 높이가 변하기 때문에 키보드가 완전히 올라왔을 때 높이를 구하려면 UIKeyboardFrameEndUserInfoKey를 사용해야 한다.

그리고 keyboardWasShown(_:) method에서 전달받은 notification에서 키보드에 대한 정보에 접근해서 keyboardSize를 구했다.

2. UIScrollView의 contentInset 조정

코드를 보기 전에 contentInset이 뭔지를 간단하게 보자. contentInset은 이름에서부터 짐작할 수 있듯이 컨텐츠의 inset, 즉 내부에 삽입하는 것과 관련된 것이구나 생각할 수 있다.

image

contentInset은 safe area나 scroll view 끝에서 content view가 안에서 얼마나 떨어졌는지에 대한 거리를 의미한다. contentInset은 UIEdgeInset 타입으로, 아래와 같이 아래, 왼쪽, 오른쪽, 위쪽에서 얼마나 떨어져 있는지를 나타낸다.

image

참고로 contentInset의 기본값은 UIEdgeInsetsZero다.

아무튼 scrollView의 contentInset을 어떻게 조정해야 할까? 우리가 하려는 것은 키보드가 올라왔을 때 키보드 아래에 가려지는 부분을 키보드 위로 올리는 것이다.

그러면 scrollView가 있고 내부에 content view가 있다면 이 contentView를 위로 올려야 한다. 이는 scrollView의 contentInset의 bottom에 키보드 높이만큼의 값을 줘서 할 수 있다. 그림으로 나타내면 아래와 같다.

image

더 정확하게 표현하면 아래와 같이 될 것이다.

image

녹색은 화면을 표현한 것이고, content view는 scrollView의 content view를 표현했다. content view의 길이가 길어서 스크롤 뷰에서 스크롤을 통해 볼 컨텐츠의 위치를 조정한다. 만약 위 화면에서 사용자가 아래로 스크롤하면 아래와 같이 될 것이다.

image

화면상에서 하단 부분에는 contentInset이 보일텐데, 실제로 저 부분은 여백이므로 빈 부분으로 보일 것이다. 그렇다면 키보드가 올라왔을 때 어떻게 될까? 우리는 contentInset을 키보드의 높이만큼 줬다. 따라서 키보드가 올라와 있으면 아래와 같이 될 것이다.

image

즉 우리가 줬던 contentInset 만큼의 빈 공간은 키보드가 덮어버리게 되고, contentView는 실제로 키보드 위로 올라간 것처럼 보이게 되는 것이다.

코드 상으로는 아래와 같이 조정한다. 위에서 작성한 keyboardWasShown(_:) 메서드 내에 scrollView의 contentInset과 scrollIndicatorInset의 bottom 값을 키보드의 높이만큼 설정해준다. 참고로 scrollIndicatorInset은 스크롤할 때 나타나는 스크롤 바의 위치가 스크롤 뷰 내에서 얼마나 떨어져 있는지를 나타낸다.

@objc private func keyboardWasShown(_ notification: Notification) {
    let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue

    if let keyboardSize = keyboardFrame?.cgRectValue {
        let inset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
        scrollView.contentInset = inset
        scrollView.scrollIndicatorInsets = inset
    }
}

그리고 앱을 실행시키고, textView 내부를 클릭해보면 아래와 같이 나온다.

image

텍스트 뷰가 안 올라간게 당연하다. 우리는 content view의 하단 여백이 얼마나 떨어졌을지만을 조정했지 실제로 content view를 얼마만큼 위로 스크롤 할 지 설정하지 않았기 때문이다. 즉 현재 아래와 같은 상황이다.

image

content view의 밑에 padding을 줬고, content view에 우리가 위로 올리고 싶은 textView가 있다. 그리고 textView를 클릭해서 키보드가 올라오면 textView가 가려지게 된다. 우리가 여기에서 추가로 해줘야 할 것은 scrollView를 실제로 얼마나 스크롤 할 지 추가로 지정해줘서 textView를 올려야 하는 것이다.

추가로, 키보드가 내려갔을 때 contentInset을 다시 원래대로 설정해야 한다. 먼저 키보드가 내려갔을 때 contentInset을 모두 0으로 만들어주는 keyboardWillBeHidden method를 작성한다. 이 메서드는 키보드가 내려갔을 때 UIScrollView의 contentInset을 다시 0으로 만들어서 원래대로 만든다.

@objc private func keyboardWillBeHidden(_ notification: Notification) {
    scrollView.contentInset = UIEdgeInsets.zero
}

그리고 이 method를 핸들링할 observer를 등록한다.

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)

그리고 앱을 실행시키면 아래와 같이 나온다. UITextView 안을 클릭하면 키보드가 올라오면서 UIScrollView 하단에 inset이 생기고, 스크롤 해서 끝까지 내릴 수 있다. 그리고 키보드를 다시 해제하면 inset이 0으로 바뀌면서 스크롤 할 수 없게 된다.

Simulator Screen Recording - iPhone 12 Pro Max - 2022-04-06 at 08 23 58

3. UIScrollView의 contentOffset 조정하기

위에서 얘기한 scrollView를 얼마나 스크롤할지에 대한 것은 contentOffset을 통해 조정할 수 있다.

contentOffset은 무엇일까?

image

contentOffset은 content view의 origin이 scroll view의 origin으로부터 얼마나 떨어져 있는지를 나타낸다. 얘도 마찬가지로 기본값은 CGPointZero다. UIScrollView는 세로로 스크롤 하고 싶을 때 contentOffset.y의 값을 조정하여 컨텐츠의 어떤 부분을 보여줄 지를 결정한다.

다시 본론으로 돌아와서 키보드가 올라왔을 때 UITextView를 키보드 위에서 20만큼 떨어진 곳까지 스크롤 하고 싶으면 어떻게 해야 할까?

keyboardWasShown method에 contentOffset을 설정하는 부분을 작성한다.

let textViewTopY = keyboardSize.origin.y
let textViewBottomY = textView.frame.origin.y + textView.frame.height

# 키보드 위에서 20만큼 떨어진 곳까지 UITextView를 올린다(UIScrollView를 스크롤한다.)
scrollView.contentOffset.y =  textViewBottomY - textViewTopY + 20

그리고 앱을 실행시키면 아래와 같이 나온다.

Simulator Screen Recording - iPhone 12 Pro Max - 2022-04-06 at 08 35 36

보면 키보드가 올라간 뒤에 스크롤이 돼서 굉장히 부자연스럽다. 이건 notificationCenter에 observer를 등록할 때 아래와 같이 등록했기 때문이다.

NotificationCenter.default.addObserver(self, selector: #selector(keyboardWasShown(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)

name에 UIResponder.keyboardDidShowNotification을 썼는데, 이건 키보드가 완전히 올라오고 난 뒤에 등록한 selector를 호출하겠다는 의미다. 이 name을 UIResponder.keyboardWillShowNotification로 바꾼다. 이러면 키보드가 올라오려고 할 때 selector가 호출된다. 이러고 앱을 실행시키면 아래와 같이 키보드가 올라오면서 contentInset이 조정되는데, 이 편이 더 자연스럽게 보인다.

Simulator Screen Recording - iPhone 12 Pro Max - 2022-04-06 at 08 39 59

  • 출처
  • https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html