iOS) UILabel baseline 문제

프로젝트를 진행하던 도중, 아래와 같은 화면을 만들어야 했다.

UILabel과 언더라인 뷰를 하나의 스택뷰로 만들어서 UILabel안에 입력된 크기에 따라 width가 변하는 하나의 스택뷰를 만들기로 결정. 글자크기는 80pt로 주고 스택뷰로 묶어서 만든 결과

이런 결과가 나왔다. 상하 여백이 넓게 존재해, 디자인한 UI와 맞지 않는다. 

처음에는 이를 해결하기 위해 sizeToFit()함수를 사용했지만 여전히 똑같이 나온다.

글자가 UILabel의 중심에 있는것으로 보아, 이를 설정할 수 있는 변수가 존재한다고 생각하고, 공식 문서를 뒤져봤지만 존재하지 않았다. 몇 시간의 삽질 끝에 원인을 찾아냈다. 

이런 문제가 발생한 이유

원인은 UILabel 자체가 아닌 font자체에 있다. 영어 노트를 생각해 보면 쉽다. 

폰트를 이해하기 위한 사진

UILabel에 텍스트를 그릴때, 위 사진과 같이 BaseLine을 기준으로 그려지게 된다. 영어가 많이 쓰여서 그런지, 영어를 기준으로 그려진다. 한글이나 숫자는 BaseLine 아래로 내려가는 글자가 없기에 Descent가 빈칸으로 남게 되고, 이게 여백이 된다. 글자가 작을 경우. Descent의 길이도 작아지니 티가 잘 나지 않지만, 글자 크기가 커지면서 Descent의 길이 역시 길어지니, 상하 여백의 크기 역시 커지게 된다.

 

그래서 숫자와 한글의 경우 descent가 필요 없으니 baseLine을 아래쪽으로 옮겨서 원하는 UI를 구현해보았다.

UILabel에서는 실제 위 사진과 같이 lineHeight, ascender 등이 정의된다. BaseLine을 아래쪽으로 내리려면 descender만큼 오프셋을 줘야 할것 같다. descender만큼 오프셋을 줘보았다.

descender는 베이스 라인을 기준으로 한 y 오프셋 값이다.

 

baseLine의 오프셋은 NSAttributedString으로 attributes를 설정해주었다. 

let font = UIFont.boldSystemFont(ofSize: 80)
let attr: [NSAttributedString.Key: Any] = [.font: font, .baselineOffset: font.descender]
let attrString = NSAttributedString(string: "Jjifg", attributes: attr)
label.attributedText = attrString

 

offSet 설정 안했을 때
offset을 descender로 설정했을때

그랬더니 Label의 Height가 늘어 났다. 제약조건은 view의 center로 두었다. 

그렇다면 font의 LineHeight나 ascender, capHeight등 font 내부의 값이 늘어난걸까? 이 역시 아니다.해당 값들을 출력해봤지만 그대로다. 

왜 이런 현상이 생기는지 생각해보았다.

 

음수를 주었는데 아래쪽으로 내려간 이유

우선 기존 UILabel은 이런식으로 baseLine이 있었을 것이다.

여기서 baselineOffset를 주면 baseLine이 세로축을 기준으로 움직인다. 우리는 descender 값을 주었다. descender는 -19.xxx값이다. 음수다!

 

여기서 뭔가 헷갈리지 않는가? UIKit의 좌표계에서 x와 y값이 양수이면은, 각각 기준의 우측, 아랫쪽으로 이동하게 된다. 그런데 baselineOffset에 음수값을 주었는데, 오히려 내려갔다.

 

그 이유는 좌표계에 있다. UILabel은 CoreText를 이용하여 글자를 렌더링 한다. 그리고 CoreText는 CoreGraphics를 이용하여 글자를 그린다. 여기서 CoreGraphics는 UIKit과 다른 좌표계를 사용한다. 

 

UIKit과 CoreGraphic의 좌표계

어떤 점을 기준으로 x와 y의 양의 값은 UIKit의 경우 우측방향, 하단 방향을 가르키지만, CoreGraphic는 우측방향, 상단 방향을 가르킨다. 따라서 baselineOffset의 값을 음수로 주면 아래쪽으로 내려가게 된다. 

descender 밑 부분이 잘린 이유

baselineOffset에 descender의 크기만큼 offset를 주었다. 그러나 그만큼 UILabel의 크기가 descender만큼 늘어났다. UILabel같이 내부 콘텐츠가 있는 뷰의 경우, 화면에 그려질 때, 내부 콘텐츠에 따라 크기가 결정된다. 하지만 이상하지 않는가? baseLine이 descender만큼 아래쪽으로 이동해 UILabel의 크기가 늘어나면 아래 사진과 같이 되야 한다.

하지만 나온 결과물은 baseline이 descender의 크기만큼 아래쪽으로 내려간게 아니라 UILabel의 아랫 부분에 붙어있다. 글자크기를 키워도, 줄여도 같은 결과가 나온다. 왜 이런 현상이 발생했을까?

몇시간동안 찾아 봤지만, 딱히 나와 있지 않아서 내 추측을 써본다. 문서 아카이브의 CoreText 레이아웃의 Listing 2-2를 보면 마지막 부분에 

// Set text position and draw the line into the graphics context
CGContextSetTextPosition(context, 10.0, 10.0);
CTLineDraw(line, context);
CFRelease(line);

이런식으로 나와 있다. 텍스트의 위치를 설정하고 context에 라인을 그린다는데, 여기서 말하는 라인이 baseLine이 아닐까 생각한다. CGContextSetTextPosition을 찾아보면 마지막 인자값이 텍스트를 그릴 y의 좌표값이라 되어 있다.

따라서 baseline을 그릴 때, CGContextSetTextPosition의 y의 좌표값을 이용해 그리는데, 여기서 baselineOffset은 이 y의 값에 offset을 적용한 y의 값을 설정하는 key가 아닐까 생각한다. 

 

우리는 상위 레벨의 라이브러리를 사용하기에 이런것들을 일일히 계산해 줄 필요가 없다. font의 사이즈만 결정해주면, UIKit이 모든 좌표값을 계산해 줄것이다. 즉 baseline의 y 좌표값은 font의 사이즈에 의해 이미 계산 되었고, descender나 lineheight값들 역시 모두 계산되있는 상태일것이다.

 

그러면 descender만큼 baselineOffset를 주었는데 baseLine이 밑부분에 붙은 이유도 설명이 가능하다.

초기 상태에서는 UILabel에 폰트의 사이즈가 정해졌을때, UILabel 내부에 baseline의 좌표값이 정해져 있는 상태일것이다.

UILabel 내부에 baseLine 좌표값이 존재하지만, offset만큼 y값을 이동시킨다. 여기서는 descender만큼 y값을 이동 시켰으니 기존 baseline의 y값은 0이 되었을 것이고, 따라서 UILabel의 아랫부분에 baseLine이 위치하게 된다.

그리고 내부 콘텐츠의 크기가 변경 되었으니 UILabel의 크기는 그에 맞춰지게 된다.

 

그렇다면 어떻게 해야 UILabel의 크기는 그대로 유지하되, baseLine의 높이만 변경 할 수 있을까?

 

첫번째 시도

우선은 UILabel의 높이를 고정시켜 보았다.

높이는 고정 되었다. 그런데 뭔가 이상하다. baseLine보다 더 많이 잘렸다.

UILabel의 크기를 고정시켰더니, UILabel 윗부분을 기준으로 잘렸다. 윗 사진의 형광색 부분이 현재 UILabel이다. 뷰를 그릴때 UIKit는 왼쪽 상단을 원점으로 잡는다. 따라서 왼쪽 상단을 중심으로 크기가 잡히게 되니 잘려버린것.

두번째 시도

font 자체의 lineHeight 같은 요소들은 그대로니, sizeToFit()를 쓰면 되지 않을까?

이것도 안된다.

원인

자료를 좀 더 찾아보니, text가 그려질 때 text는 NSTextParagraph 내부에 그려진다. UILabel, TextField, TextView 모두 NSTextParagraph를 사용한다. sizeToFit는 이런 내부 NSTextParagraph의 크기에 맞춰서 UILabel의 크기를 조절한다. baselineOffSet에 의해 NSTextParagraph가 늘어났고, 그에 따라 각 줄의 단락역시 늘어나 이런 현상이 생긴 것이다. 

 

해결

즉 NSTextParagraph에 의해 UILabel의 크기가 결정되는 것이니 이것을 조절하면 되겠다 싶었다.

따라서 NSTextParagraph는 NSMutableParagraphStyle를 이용해 높이를 조절하면 된다.

let style = NSMutableParagraphStyle()
style.maximumLineHeight = label.frame.height
style.minimumLineHeight = label.frame.height
let font = UIFont.boldSystemFont(ofSize: 80)
let attr: [NSAttributedString.Key: Any] = [.paragraphStyle: style ,.font: font,.baselineOffset: font.descender]
let attrString = NSAttributedString(string: "pjivq", attributes: attr)
label.attributedText = attrString

 

참고 자료: https://developer.apple.com/documentation/foundation/nsattributedstring,

https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/CoreText_Programming/LayoutOperations/LayoutOperations.html#//apple_ref/doc/uid/TP40005533-CH12-SW3,

https://developer.apple.com/documentation/uikit/text_display_and_fonts,

https://developer.apple.com/documentation/uikit/nstextparagraph