CLOSE SEARCH

Text Kit : focus on Legibility and easy API #1

Apple은 사용자들이 iOS 디바이스를 통해 원하는 내용을 더욱 또렷하게 볼 수 있도록 다양한 노력을 하고 있습니다. 2010년 레티나 디스플레이를 통해 하드웨어적인 개선을 이루었다면 올해는 Text Kit을 통해 소프트웨어적인 개선을 이루었다고 할 수 있습니다. Text Kit은 특히 개발자에게 사용하기 쉬운 API를 제공하기 때문에, Pages와 같이 복잡한 포멧팅을 지원하는 앱도 이전보다 더욱 쉽게 개발할 수 있게 되었습니다.

Little History

iOS 6까지 텍스트 출력을 담당하는 UI객체들은 Core Text, WebKit, String Drawing을 기반으로 구현되었습니다. 이런 구현은 iOS에서 충분히 잘 동작했지만, 몇가지 문제점을 안고 있었습니다. String Drawing의 경우 텍스트 출력을 위한 가장 쉬운 방법이지만 성능상의 문제가 있었고, Core Text의 경우 모든 개발자가 손쉽게 사용할 수 있는 API는 아니었습니다. Core Text와 같은 고급 API가 UILabel에 텍스트를 출력하는 간단한 작업에도 사용된다는 것은 분명 불필요한 오버헤드였습니다.

text_architecture_ios6

Simplest way : String Drawing

iOS에서 문자열을 출력하는 가장 쉬운 방법은 NSString의 UIKit 확장을 사용하는 것입니다. 이 확장은 UIStringDrawing 카테고리를 말합니다. 이 카테고리는 크게 두가지 기능을 제공합니다.

[list style=”note”]

  • 텍스트 영역 측정
  • 문자열 출력

[/list]

이 방식은 주로 UIView를 상속한 클래스의 drawRect: 메소드에서 사용됩니다. 그래서 drawRect: 메소드가 호출될 때마다 문자열을 새롭게 출력하기 때문에 성능상의 불이익을 가집니다. 그래서, 단순한 텍스트를 출력할 때는 UILabel과 같은 UIKit 객체를 사용하는 것이 좋습니다. UIKit 객체는 String Drawing 방식과 달리 문자열을 출력한 후에 불필요한 재출력 과정이 필요하지 않습니다.

이와 유사하게 CATextLayer 클래스를 통해 텍스트를 출력할 수도 있습니다. 이 방식은 속성에 에니메이션을 적용할 수 있기 때문에 코어 에니메이션의 이점을 손쉽게 사용할 수 있습니다.

Advanced Unicode layout engine : Core Text

코어 텍스트는 iOS와 Mac 개발에 사용되는 유니코드 레이아웃 엔진입니다. 텍스트에 관한 거의 모든 기능을 제공하는 프레임워크로 Text Kit이 소개되기 전까지는 워드프로세서와 같은 앱을 만들기 위해서 반드시 사용해야 했습니다. 하지만 방대한 기능만큼이나 API를 자유자재로 사용하기 위해서는 많은 시간과 노력이 필요했습니다.

 

Text Kit

텍스트 킷은 코어 텍스트를 기반으로 개발되었기 때문에 동일한 성능과 기능을 제공하지만, 훨씬 배우기 쉽고 사용하기도 쉬운 API를 제공합니다. 앞으로는 코어 텍스트를 직접 사용해야 하는 경우가 거의 없을것입니다.

Apple은 이전의 텍스트 처리방식이 가지고 있던 비효율성, 복잡성, 성능상의 오버헤드를 해결하면서 사용자에게 꼭 필요한 기능만을 노출하여 쉽게 사용할 수 있는 텍스트 프레임워크를 개발하기 위해 몇년간 많은 연구를 거듭했다고 합니다. 그 결과, 코어 텍스트라는 강력하고 효율적인 텍스트 프레임워크가 탄생했고 iOS 7의 다양한 요소들과 긴밀하게 통합되면서 사용자에게 더 나은 텍스트 경험을 제공하는데 중요한 역할을 담당하게 되었습니다.

text_architecture_ios7

UILabel, UITextView와 같은 UI 객체들을 Text Kit을 사용해서 다시 작성되었다고 합니다. 특히 iOS 7의 UITextView와 UITextField는 텍스트 킷이 제공하는 모든 텍스트 속성을 지원하기 때문에 이전보다 훨씬 풍부한 텍스트를 표현할 수 있게 되었습니다. 그리고 iOS 6까지는 WebKit이 텍스트 출력에 어느정도 관여했지만, iOS 7부터는 UIWebView의 출력만을 담당하게 되었습니다. 그리고 Text Kit은 테이블 뷰와 같은 컬렉션 컨트롤과도 유연하게 동작하며, 에니메이션도 문제없이 지원합니다. 앞에서도 말했듯이 텍스트 킷은 코어 텍스트를 기반으로 했기 때문에, 대다수의 개념을 공유하며 서로 Toll-free Bridged 하게 구현되어 있습니다.

Toll-free Bridged는 하나의 데이터 형식을 특별한 작업없이 다른 형식으로 변환할 수 있는 방식을 나타냅니다. 코코아와 코코아 터치 프레임워크에서 광범위하게 사용되는 개념으로, 코어 파운데이션 타입과 Objective-C 클래스 사이에 상호변환이 가능한 경우 이 두 객체는 “Toll-free Bridged 하게 구현되었다” 또는 “Toll-free Bridged 하다”라고 표현합니다. 예를 들어, CFArrayRef 형식은 NSArray 클래스로 변환할 수 있고, NSString 클래스는 CFStringRef 형식으로 변환할 수 있습니다.

이와 더불어 텍스트 킷은 자간(Kerning)과 합자(Ligature)에 대한 처리를 모든 텍스트에 적용하여 더 나은 가독성을 제공합니다.

Text kit Equation

텍스트 킷에서 자주 사용되는 용어는 다음과 같은 공식으로 풀이할 수 있습니다. 이 공식은 iOS와 Mac에서 사용하는 텍스트 기술을 이해하는데 많은 도움을 줍니다.

Characters (or String) + Font = Glyphs

Glyphs + Location = Text Layouts

 

3 Core Classes

MVC Pattern

Text Kit은 세가지 주요한 클래스로 구성되어 있고, 이 클래스들은 MVC 패턴을 따르고 있습니다. 텍스트 저장소와 컨테이너는 모델, 레이아웃 메니저는 컨트롤러의 역할을 담당하며, UITextVIew나 UILabel과 같은 객체들은 뷰의 역할을 담당합니다.

textkit_3main_component

NSTextContainer

NSTextContainer는 텍스트가 배치되는 영역을 정의합니다. 일반적으로 사각영역을 지정하지만, 서브클래싱을 통해 원하는 모양의 영역을 지정할 수도 있습니다. 그리고 exclusion zone 또는 exclusion path라고 부르는 제외영역 배열을 가지고 있으며, 이를 통해 텍스트 래핑(Wrapping)을 쉽게 구현할 수 있습니다.

NSTextStorage

NSTextStorage는 화면에 표시할 텍스트의 저장소이며, NSMutableAttributesString 클래스로부터 파생되었습니다. 이 클래스는 저장된 텍스트와 속성의 일관성을 유지하며, 자신과 연관된 레이아웃 메니저에게 변경 통지를 전달하는 역할을 담당합니다.

NSLayoutManager

NSLayoutManager는 앞에서 설명한 두 객체의 동작을 조율하는 컨트롤러입니다. 레이아웃 메니저는 텍스트 저장소에 저장되어 있는 데이터를 뷰의 가시영역에 렌더링 할 수 있는 텍스트로 변환합니다. 그리고, 출력할 유니코드 문자를 적절한 Glyphs로 맵핑하고 컨테이너 객체에 정의되어 있는 영역내에서 Glyphs의 레이아웃을 관리합니다.

 

3 Core Attributes

텍스트 킷은 3가지 범위에서 속성을 처리합니다.

Characters

먼저 문자의 속성을 처리할때는 NSMutableAttributedString(이하 속성문자열)에서 사용하는 방식을 사용합니다. 이런 방식을 사용할 수 있는 것은 텍스트 컨테이너가 NSMutableAttributedString로부터 상속되기 때문입니다.

속성 문자열은 다음과 같은 문자열을 키로 사용해서 NSDictionary 객체에 문자의 속성을 저장합니다.

문자의 속성을 지정하는 예제코드를 살펴보겠습니다.

화면에 4개의 레이블을 추가한 다음 각각 아웃렛으로 연결하고 속성이 적용된 문자열을 출력하는 간단한 코드입니다. 2번 라인에서는 NSFontAttributeName 키를 사용해서 폰트를 30pt의 시스템 폰트로 지정하고 있습니다. 5번 라인에서는 글자의 색상을 파란색으로 지정하되 range 파라미터를 통해 첫번째 글자에만 적용되도록 제한하고 있습니다. 8번 라인에서는 두번째 문자에서 마지막 문자까지 밑줄이 추가되도록 구현하고 있습니다. 이렇게 addAttribute:value:range: 메소드를 통해 하나의 문자 또는 원하는 범위에 속성을 지정할 수 있습니다. value로 전달되는 각 속성은 속성의 종류에 따라 달라지므로 잘 기억하고 있어야 합니다. 만약 여러개의 속성을 동시에 지정하고 싶은 경우에는 11~14번 라인과 같이 NSDictionary 객체에 키와 값을 구성한 뒤 addAttributes:range: 메소드에 전달하면 됩니다.

예제코드를 실행해보면 지정한 방식대로 속성이 적용되어 있는것을 확인할 수 있습니다.

change_char_attributes

사실 앞에서 설명한 예제는 iOS 6에서 속성문자열을 편집하는 것으로, 정확하게 말하면 텍스트 킷을 사용했다고 볼 수는 없습니다. 텍스트 킷을 사용해서 속성을 변경하기위해 속성문자열을 편집할 때 사용했던 방법을 그대로 사용할 수 있지만, 몇가지 작업이 더 필요합니다.

이 예제코드는 화면에 있는 텍스트뷰의 문자 속성을 변경하는 코드입니다. 가장 먼저 텍스트뷰의 textStorage 속성을 통해 텍스트 저장소에 대한 참조를 가져옵니다. 앞에서도 설명했듯이 이 텍스트 저장소는 NSMutableAttributedString 클래스를 상속하기 때문에 iOS 6에서 속성문자열을 편집할 때 사용했던 방식과 동일한 방식으로 작업할 수 있습니다. 가장 중요한 핵심은 4번과 10번 라인입니다. 4번 라인에서는 속성을 변경하겠다는 것을 알려주고, 11번 라인에서는 속성 변경을 완료했다는 것을 알려줍니다. 텍스트 저장소는 endEditing 메소드가 호출되면 자신의 델리게이트에 다음과 같은 메시지를 보내고, 내부에서 processEditing 메소드를 호출해서 변경사항을 적용합니다. 이 때, 적용된 속성 사이에 모순이 있다면 Attribute Fixing이라는 방식(fixAttributesInRange: 메소드 호출)을 사용해서 모순을 제거합니다.

내부적으로 작업이 완료되면 다시 델리게이트에 메시지를 보내고 추가 작업을 할 수 있도록 해줍니다.

텍스트 저장소는 모든 작업이 완료되면 자신과 연관된 레이아웃 메니저로 processEditingForTextStorage:edited:range:changeInLength:invalidatedRange: 메시지를 보내며, 레이아웃 메니저는 이 메시지를 통해 전달된 정보를 바탕으로 텍스트의 위치를 재조정하거나 재출력하는 작업을 수행합니다.

 

Paragraphs

NSParagraphStyle 클래스를 통해 구현되는 단락속성은 레이아웃 메니저가 라인을 정렬하는 방식에 영향을 줍니다. 단락속성을 변경하는 방법은 문자속성을 변경하는 방법과 동일하며, 속성키로 NSParagraphStyleAttributeName을 사용합니다.

이 예제코드는 텍스트를 중앙정렬로, 줄의 간격을 20으로 지정하고 있습니다.

[one_half]

No Paragraph style

change_paragraph_attribute_before

[/one_half]

[one_half_last]

Center alignment, Line spacing 20pt

change_paragraph_attribute_after

[/one_half_last]

실행결과에서 한가지 주목할 점은 단락속성을 20번째 문자까지 지정했지만 결과적으로는 단락의 나머지 문자에도 모두 적용되었다는 것이니다. 이것은 Attribute Fixing이 동작하는 방식때문이며, 이번 예제와 같이 단락의 일부에만 속성이 적용된 경우 전체적인 일관성을 유지하기 위해 단락의 나머지 문자에도 동일한 속성을 적용시켜줍니다.

 

Documents

문서의 속성에는 페이지의 크기, 여백의 크기, 확대 비율 등이 있습니다. 앞에서 설명한 두가지 속성과 달리 문서의 속성을 저장하고 변경하는 매커니즘은 아직까지 지원되고 있지 않습니다. 앞으로 Text Kit에 많은 개선이 이루어지면서 새로운 클래스를 통해 내장지원을 제공하게 될 것 같습니다. 현재로써는 NSAttributedString 클래스에서 제공하는 initWithRTF:documentAttributes: 메소드에 NSDictionary 형태로 속성을 설정하는 방법만을 사용할 수 있습니다. (iOS 7 Beta SDK에는 아직 구현되어 있지 않은 상태입니다.)

Filed under: Apple WWDC 2013, iOS