[iOS] 키보드 레이아웃을 가져오는 개선된 방법 (NotificationCenter to Keyboard Layout Guide)

2024. 8. 28. 00:12UIKit, SwiftUI, H.I.G

 

[UIKit] 재사용 Component 개발하기 (3) - 바텀 시트 (Sheets, Bottom Sheet)

[UIKit] 재사용 Component 개발하기 (1) - Toast Message💡 재사용 Component 개발하기 시리즈 글을 시작하며 이번 33기 앱잼이었던 프로젝트에서 우리 iOS 팀이 추구한 방향 중 하나는, 앱 내에서 반복되는 컴

mini-min-dev.tistory.com

오늘 글은 예전 위의 글에서 설명했던 바텀 시트(Sheets)의 기능을 개선하기 위해 공부한 내용이다.

위의 글을 안 읽은 분들이 있을 것 같아 필요한 간략한 개념만을 설명해 보자면, (바텀시트 만드는 전반적 과정은 위의 글에서 설명했다!)
내가 참여했던 <TOASTER 토스터>라는 프로덕트에는 아래 이미지에서 보이는 것처럼 매우 다양한 바텀시트가 활용되고 있었다.

나 같은 경우에는 바텀시트 인스턴스를 찍어낼 때 바텀시트의 높이 값(bottomHeight)을 받았다.
시트(Sheets)가 밑에서 위로 올라올 때 "얼마큼의 높이만큼 올라와야 하는지를 직접 지정해주기 위해서" 값을 받았고, 이때 받은 높이 값(bottomHeight)은 바텀시트 뷰(bottomSheetView)의 레이아웃 제약 조건을 잡는 데 바로 사용했다.
*바텀시트의 높이 또한 내부에 들어가는 콘텐츠만큼이나 매우 가지각색인 것을 위의 캡처 화면에서 확인할 수 있다.

bottom(아래), leading(뷰 기준 좌측), trailing(뷰 기준 우측)은 상위 뷰인 ViewController 자체의 View와 동일하게 설정하지만,
top 부분은 전체 뷰의 높이에서 바텀 시트의 높이(bottomHeight) + 키보드의 높이(keyboardHeight)를 제외한 만큼 상위 뷰와 간격(inset)을 두고 잡히도록 코드를 작성했다.
이 내용은 아래 그림에서 추가적으로 각 부분을 설명하고 있으니 참고해 보도록 하자.

*키보드가 존재하는 경우는 키보드의 높이만큼 마이너스가 될 것이고, 키보드가 존재하지 않는 경우에는 키보드의 높이가 0이니 신경 쓰지 않아도 되는 셈!
**지금 아래의 코드처럼 top 제약조건을 잡는 방법 말고도, 그냥 height 값(+ 키보드 높이 포함)을 바텀시트의 높이 값으로 지정해 줘도 될 뻔했다.

// 바텀시트 내부 프로퍼티
private var bottomHeight: CGFloat
private var keyboardHeight: CGFloat

// 바텀시트 생성 시 초기화
init(height: CGFloat ..) {
    self.bottomHeight = height
}

// 바텀시트 표출 시 호출
bottomSheetView.snp.makeConstraints {
    $0.bottom.leading.trailing.equalToSuperview()
    $0.top.equalToSuperview().inset(self.view.frame.height - self.keyboardHeight - self.bottomHeight)
}
self.view.layoutIfNeeded()

바텀 시트의 top Constraints를 계산하는 방식이다. (view.frame.height - bottomHeight - keyboardHeight)


"굳이 바텀시트의 높이(bottomHight)와 키보드의 높이(keyboardHeight)를 분리할 필요가 있나" 싶기도 하겠지만, 키보드는 고정된 높이를 갖지 않는다는 큰 이슈(issue)가 있어 각 높이를 분리해서 적용하는 방법을 떠올리게 되었다.

여기서 "키보드가 고정된 높이를 갖지 않는다"는 것의 원인으로 볼 수 있는 것들로는,

  • 언어, 이모티콘, 자동 완성 등 키보드를 사용하는 다양한 시나리오별로 사용되는 키보드의 레이아웃이 다르다는 점
  • 다양한 기기(mini vs pro max), 플랫폼(iOS vs iPadOS), 심지어는 레이아웃 모드(가로 vs 세로) 별로 표출되는 키보드가 다르다는 점 등이 있다.

키보드의 높이 값은 애초에 절대값이 아니다. 여기에 기기별 다른 크기까지 합쳐지면 더더욱 절대값을 가질 수 없다!

즉, 어떤 상황 / 플랫폼 / 기기 / 모드에서도 올바른 바텀시트(Sheets)의 높이를 지정할 수 있도록 키보드 레이아웃 값을 가져와야 한다는 목표가 존재했다.

조금 돌아오긴 했는데, 아무튼 이번 글에서 다룰 주제는 위의 목표를 달성하기 위한 "키보드의 레이아웃 값을 가져오는 방법"에 대한 내용이다.
지금부터는 NotificationCenter를 통해 키보드의 높이 값을 추적했던 기존 방법에서 - iOS 15부터 사용 가능해진 Keyboard Layout Guide라는 새로운 방법으로 코드를 개선하는 과정까지 차근차근 살펴보도록 하겠다.

 

1️⃣ NotificationCenter와 UIResponder로 키보드 높이 가져오기

가장 일반적인 방법으로는 NotificationCenter와 UIResponder를 통해 키보드의 상태를 관찰(Observe)하며 상태에 맞는 키보드 레이아웃 값을 받아오는 방법이 있다.
여기서 말하는 관찰(Observe)이란? 현재 시스템에서 키보드가 올라와있는지(Show) / 숨겨져 있는지(Hide) 여부를 관찰하는 것을 의미!

우선, NotificationCenter를 통해 키보드가 나타나거나/사라질 때의 상태를 감지하고 - 동시에 호출되는 메서드를 연결해보도록 하겠다.
NotificationCenter로 키보드의 상태를 추적할 때는 UIResponder라는 앱 내 이벤트 처리 클래스가 사용된다.

  • UIResponder.keyboardWillShowNotification : 키보드가 화면에 나타날 때 발생하는 알림 (정확하게는 키보드가 나타나기 직전에 발생)
  • UIResponder.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)

다음으로는 NotificationCenter의 addObserver 코드에서 연결했던 메서드들을 구현할 차례이다.

키보드가 사라지는 상황은 제외하고,
키보드가 표출되는 상황에서 notification의 userInfo라는 딕셔너리에서 키보드와 관련된 정보를 꺼내오는 상황만 집중해서 살펴보겠다.
설명 들어간다~!

  1. UIResponder.keyboardFrameEndUserInfoKey : 키보드의 최종 프레임을 나타내는 값으로, 키보드가 나타날 때의 크기와 위치 정보를 담고 있는 Key값이다. 타입은 CGRect 값으로 받게 될 것.
  2. keyboardSize.height : 이렇게 꺼내게 된 keyboardSize CGRect 값 중에서 높이(height)를 얻을 수 있다.
  3. updateBottomSheetLayout() 메서드를 통해 얻어온 키보드 높이 값을 바탕으로, 바텀시트의 레이아웃을 업데이트하는 메서드
@objc
func keyboardWillShow(_ notification: Notification) {
    let info = notification.userInfo
    if let keyboardSize = info?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
        keyboardHeight = keyboardSize.height
        updateBottomSheetLayout()
    }
}

@objc
func keyboardWillHide(_ notification: Notification) {
    keyboardHeight = // default value = 0
    updateBottomSheetLayout()
}

updateBottomSheetLayout() 메서드는 위의 바텀시트가 호출될 때 잡았던 레이아웃을 다시 잡는(remakeConstraints) 코드다.

키보드의 Show/Hide 여부에 따라 self.keyboardHeight 값에는 키보드의 높이/기본 값이 들어가게 될 것이고,
이는 이 글 처음에서 소개했던 것처럼 바텀시트가 얼마큼 올라갈 것인가를 결정하는 레이아웃 제약조건에 영향을 미치게 된다고 보면 된다.

func updateBottomSheetLayout() {
    bottomSheetView.snp.remakeConstraints {
        $0.bottom.leading.trailing.equalToSuperview()
        $0.top.equalToSuperview().inset(self.view.frame.height - self.keyboardHeight - self.bottomHeight)
    }
    self.view.layoutIfNeeded()
}

 

2️⃣ KeyboardLayoutGuide로 키보드 높이 가져오는 로직 개선하기

이렇게 복잡했던 과정을 iOS 15부터 사용할 수 있게 된 Keyboard Layout Guide를 사용하면, 정말 간단하게 줄일 수 있다.

위에서 구현했던 NotificationCenter와 키보드의 Show/Hide 여부와는 상관없이
단순히 바로 keyboardLayoutGuide라는 레이아웃 가이드를 통해 키보드의 크기와 위치를 쉽게 받아와 레이아웃 제약조건을 설정하고 있는 것이 아래 코드이다.

+ SnapKit을 사용하지 않을 경우, view.keyboardLayoutGuide.topAnchor와 같이 일반적인 레이아웃을 잡는 것처럼 활용하면 된다.

bottomSheetView.snp.remakeConstraints {
    $0.leading.trailing.equalToSuperview()
    $0.bottom.equalTo(view.keyboardLayoutGuide.snp.top)
    $0.height.equalTo(bottomHeight)
}


(+ iOS 17 추가사항)

WWDC23 <Keep up with the keyboard> 세션에서 keyboardLayoutGuide에 대한 몇 가지 추가 속성을 함께 설명한다.
속성들 중 usesBottomSafeArea라는 속성을 사용한다면, 키보드를 사용하지 않는 바텀 시트의 bottom 제약 조건을 제대로 설정해줄 수 있다.

💡 usesBottomSafeArea는 기존 keyboardLayoutGuide가 view의 Safe Area Layout Guide를 사용할지/말지를 정할 수 있는 Boolean 속성이다.

이 값은 기본적으로 true이기 때문에 이전까지 keyboardLayoutGuide를 사용하는 경우에는 필연적으로 Safe Area Layout Guide를 따라야 한다는 단점이 있었다.
나 같은 경우에는 키보드 높이를 사용하지 않는 경우도 있었다고 했는데 - 그런 상황에서는 아래 화면처럼 바텀시트의 bottom 레이아웃이 키보드 상단의 top 레이아웃을 Safe Area 기준으로 맞추기 때문에, 그 공간이 비어보이는 듯한 문제가 생길 수밖에 없었던 것!

아래 Bottom이 떨어져 있는 거 보이나?!

iOS 17 버전 이상부터는 이 값을 false로 설정해 SafeArea를 무시하도록 만들 수 있게 되었다.

+ 키보드에 대한 설명은 매년 WWDC에서 소개될 정도로 Apple에서 꽤 진심으로 다루고 있다는 점이 신기했다.
++ 확실히 기존 코드에 대한 불편함을 개선하기 위한 노력이 지속되고 있다는 점, 그리고 이것들은 Apple의 Multi Platform App의 발전과도 이어지고 있다는 점 등을 이번 키보드 높이 계산 코드를 통해 배워본다.

if #available(iOS 17.0, *) {
    view.keyboardLayoutGuide.usesBottomSafeArea = false
}

SafeArea를 무시해 올바른 높이를 잡게된 모습!