채팅 입력창 같은 경우 사용자가 입력한 메시지의 라인 수에 따라 채팅창 높이가 동적으로 변경되는 UI를 흔히 볼 수 있다. 메시지 입력 시 높이 조절과 스크롤이 이상하게되는 문제를 해결하다 발견한 내용을 짧게 남긴다.
레거시 코드는 UITextView.contentSize.height
를 텍스트높이로 사용하고 있었는데 텍스트가 여러줄이 되면서 정확한 값을 주지 않고 있었다. 검색해보니 iOS 7 이전에 사용하던 방식이라네.
그래서 -[NSLayoutManager usedRectForTextContainer:]
요놈을 사용하려고 했는데 값을 잘 주는가 싶더니 라인개행이 될 때 마다 한번씩 더 작은 값을 던져줘서 사용 포기, 왜 이러는거지? 어쩔 수 없이 역시나 -[NSAttributedString boundingRectWithSize:options:context:]
를 사용해야지.
채팅창을 최대 3줄로 만든다고 하면 대충 이런 식
func textViewDidChange(_ textView: UITextView) {
let lineHeight = font.lineHeight
let lineSpaing = textView.lineSpacing
let insets = textView.textContainerInset.top + .bottom
// inset 추가 없어도 4줄 이하 판별은 문제 없어 보임
let lines = 4
let heightFor4Lines = (lineHeight * lines) + (lineSpaing * (lines - 1))
let margins = 텍스트 뷰의 컨터이너 뷰 마진
let paddings = 텍스트 뷰의 컨터이너 뷰 패딩
let originalTextViewContainerHeight = // 텍스트뷰와 UI를 위해 감싸는 컨터이커 까지의 높이
// textView.textContainerInset.left/right 값이 0이 아니라면 boundingRect 구할 때 적용해야 할 수도 있음
let boudingRect = textView.attributedText.boudningRect...
let textHeight = ceil(boudingRect.height)
if textHeight < heightFor4Lines {
// 1~3 라인은 동적으로 높이 변경
let height = max(originalTextViewContainerHeight, max(defaultTextViewHeight, textHeight) + margins + paddings + insets)
updateTextContainerHeight(height)
} else {
// 4라인 이상은 마지막 높이(3라인) 유지하고 하단 스크롤만 시킴
scrollTextViewToBottom()
}
}
func updateTextContainerHeight(height) {
guard pre.height != height else { return }
// TODO: update height
scrollTextViewToBottom()
}
func scrollTextViewToBottom() {
// TODO: scroll text view to bottom
}
}
요약하자면,
- line spacing, line height, inset, margins, padding 등으로 높이를 구하고
- 1~3 라인이고, 구한 높이값이 이전 높이와 다르다면 변경하고
- 텍스트뷰를 최하단으로 스크롤 시킨다.
텍스트뷰를 최하단으로 스크롤 시키는 이유는 라인 개행 시 텍스트뷰 상단에 윗 글자의 하단 부분이 살짝 보일 수 가 있는데 이 부분을 위로 올리기 위함이다. 물론 개행 후 하단에 스크롤 시킬 영역이 남아있어 가능한 일이겠지( 이 스크롤가능한 영역이 inset.bottom 값과 관련있나?. 다른 차원의 나는 두 값을 비교해 보자).
최하단 스크롤 내용은 채팅창 UI 디자인에 따라 필요없는 내용이 될 수 도 있겠지만, 필요할 때는 도움이 되는 작은 꿀팁 정도로 알아두자.