CLOSE SEARCH

Text Kit : focus on Legibility and easy API #3

Characters vs Glyphs

Character와 Glyph는 모두 문자를 나타내는 용어이지만, 이 둘 사이에는 뚜렷한 특징이 있습니다. Character의 경우 저장된 문자 그 자체를 나타내며, 텍스트 킷에서는 NSTextStorage에 저장되어 있는 문자데이터를 나타냅니다. 이에 비해 Glyph는 저장된 문자에 폰트가 적용되어 실제로 화면에 출력되는 문자입니다. 그래서 Glyph는 적용된 폰트에 따라서 모양이 완전히 달라지게 됩니다.

char_vs_glyphs

텍스트 킷은 이전 글에서도 언급했던 것처럼 가독성 향상을 위해 모든 텍스트의 자간(Kerning)과 합자(Ligature)를 스스로 조절합니다. 그래서 위의 그림과 같이 3개의 character는 하나의 glyph로 표현될 수도 있습니다. 그렇기 때문에 동일한 텍스트를 나타내는 경우에도 시스템이 인식하는 문자(열)의 길이나 범위는 달라질 수 있습니다. 인덱스를 통해 특정 문자를 지정하는 방식을 사용하는 경우에는 이러한 character와 glyph의 차이점을 이해하고, 텍스트 킷이 제공하는 메소드를 적절히 활용해야 합니다.

NSLayoutManager는 Glyph 정보를 가져오고, 문자 정보로 변환할 수 있는 다양한 메소드를 제공합니다.

특정 인덱스에 있는 Glyph를 가져오고 싶은 경우에는 1번 라인의 glyphAtIndex: 메소드를 사용합니다. glyphIndex 파라미터는 반드시 유효한 범위 내에 있어야 하므로, 이 메소드를 호출하기 전에 항상 Glyph 인덱스의 범위를 확인하는 것이 좋습니다.

Glyph의 인덱스는 문자열 처리코드에서 자주 사용하던  문자 인덱스와 항상 1:1로 매칭이 되는것은 아닙니다. 앞에서 설명한 합자(Ligature)나 하이픈으로 연결된 경우(Hyphenation) 두 인덱스의 값은 언제라도 달라질 수 있습니다. 문자 인덱스나 Glyph의 인덱스를 확인할 때는 2~3번 라인의 메소드를 사용합니다.

위의 그림에서 왼쪽에 있는 문자(Character) ffi의 범위는 {0, 3}으로 인식되고, 오른쪽 Glyphs는 조절된 자간과 합자로 인해 {0, 1}로 인식됩니다. Glyphs의 범위를 실제 Characters 범위로 변환하는데는 4번 라인의 메소드가 사용됩니다. 5번 라인의 메소드는 문자 범위를 Glyph 범위로 변환합니다.

 

Layout

NSLayoutManager는 레이아웃과 관련된 다양한 정보를 관리합니다. 먼저 레이아웃 메니저는 Glyph를 출력할 텍스트 컨테이너를 배열로 관리합니다. 각 컨테이너는 인덱스를 통해 접근할 수 있고, 제공되는 메소드를 통해 새로운 컨테이너를 추가하거나 삭제할 수 있습니다.

특정 위치에 있는 Glyph를 출력하는 텍스트 컨테이너의 인덱스를 얻거나 텍스트 컨테이너에서 실제로 출력에 사용된 영역의 크기를 확인할 수도 있습니다.

컨테이너를 조금 더 파고 들어가보면 Glyph들의 라인으로 구성되어 있습니다. 그러나 이런 라인들은 Attachments(첨부이미지)나 Exclusion Path에 의해 두개 이상으로 분리될 수 있습니다. 텍스트 킷에서는 이렇게 하나의 라인에서 분리된 영역을 Line Fragment라고 부릅니다. Line Fragment를 통해 원하는 Glyph가 출력된 위치를 좀 더 세밀하게 찾을 수 있습니다.

마지막으로 Glyph 자체의 레이아웃 정보를 가져올 수도 있습니다.

앞에서 설명한 레이아웃 정보들은 모두 텍스트 킷의 좌표체계를 따릅니다. 특히, 텍스트의 출력 영역을 지정하는 텍스트 컨테이너는 View와 동일한 좌표체계를 가지고 있기때문에  이해하는데 어려움은 전혀 없습니다. 텍스트 컨테이너에 포함되는 Line Fragment는 텍스트 컨테이너에 상대적인 좌표와 내부에서 Glyph 배치에 사용되는 좌표체계를 가집니다. 그리고 각 Glyph의 위치를 처리할 때는 Line Fragment의 좌표쳬계에 상대적인 Baseline 좌표를 기준으로 사용합니다.

NSLayoutManager의 델리게이트인 NSLayoutManagerDelegate를 구현을 통해 레이아웃을 좀 더 세밀하게 조절할 수도 있습니다. 이 델리게이트는 줄바꿈 방식, 라인 간격, 단락 간격 등을 문자 단위로 세밀하게 조절할 수 있는 다양한 메소드를 제공합니다.

 

앞에서 설명한 내용들을 바탕으로 몇가지 간단한 예제들을 살펴보도록 하겠습니다.

Letterpress

이전 버전까지 Letterpress 효과를 구현하기 위해서는 복잡한 코드가 필요했기 때문에 주로 이미지를 사용했었습니다. 하지만 iOS 7에서는 NSTextEffectLetterpressStyle 키를 사용해서 속성문자열(attributed string)에  Letterpress 효과를 바로 적용할 수 있게 되었습니다.

Letterpress 효과가 적용된 예는 iOS 7의 메모앱과 미리알림 앱에서 볼 수 있습니다.

[one_half]ios7_memoapp_letterpress[/one_half]

[one_half_last]ios7_reminder_letterpress[/one_half_last]

 

Text Kit Configuration

textkit_basic_configuration

가장 단순하고 기본적인 텍스트 킷 구성은 UITextView에 적용된 구성입니다. 이 구성은 텍스트 저장소(NSTextStorage), 레이아웃 메니저(NSLayoutManager), 텍스트 컨테이너(NSTextContainer) 를 각각 하나씩 가집니다. 텍스트 킷은 이러한 구성을 확장하여 이전에 쉽게 구현할 수 없었던 다양한 구성을 가능하게 해줍니다.

Multi Page & Multi Column

앞에서 살펴본 기본 구성에서 레이아웃 메니저는 하나의 컨테이너와 연관되어 있습니다. 레이아웃 메니저는 여러개의 컨테이너와 연관될 수 있기 때문에 이러한 특성을 응용하면 다중 페이지나 다중 컬럼 레이아웃을 쉽게 구현할 수 있습니다.

textkit_multicolumn_configuration

아이패드에서  3개의 컬럼을 구성하는 예제를 살펴보겠습니다. 이 예제는 텍스트 저장소와 레이아웃 메니저를 하나씩 사용하고, 레이아웃 메니저는 3개의 텍스트 컨테이너를 관리하며 각 컨테이너는 화면의 컬럼과 1:1로 매칭됩니다.

column_preview

Code3 은 멀티컬럼을 위한 기본 구성 코드를 보여줍니다. 2번 라인에서는 레이아웃 메니저를 생성합니다. 이 메니저는 3개의 컨테이너를 가지고 있으며 적절한 컨테이너에 텍스트를 출력해줍니다. 5~6번 라인에서는 텍스트 저장소를 생성하고 레이아웃 메니저를 추가해줍니다. 이렇게 추가된 레이아웃 메니저는 텍스트 저장소에 있는 텍스트를 사용해서 출력을 진행합니다. 9~11번 라인에서는 각 컬럼에 해당되는 텍스트 컨테이너를 생성합니다. 각 컨테이너는 텍스트를 출력할 영역의 크기를 정의합니다. 13~15번 라인에서는 위에서 생성한 컨테이너를 레이아웃 메니저에 추가합니다. 레이아웃 메니저는 자신과 연관된 컨테이너의 크기를 알고 있기 때문에 텍스트를 영역내에 정확히 출력해주고, 출력 공간이 모자란 경우에는 자동으로 다음 컨테이너에 출력합니다. 레이아웃 메니저가 출력하는 순서는 컨테이너가 추가된 순서와 동일합니다.

Code4 에서는 텍스트의 실제 출력을 담당할 뷰를 생성하고 현재 뷰에 추가해줍니다. 일반적으로 UITextView와 텍스트 컨테이너를 연관시켜주는 방식을 사용하지만, 뷰를 서브클래싱하여 출력코드를 직접 작성할 수도 있습니다.

위의 코드를 실행해보면 아래의 그림과 같이 각 컬럼별로 텍스트가 출력되는 것을 확인할 수 있습니다. iOS 6에서도 이와 유사한 구현은 가능했지만, 텍스트 뷰의 크기에 맞게 문자열을 자르고  각 텍스트 뷰에 문자열을 직접 지정해 주어야 했습니다. 그러나 텍스트 킷을 사용한 방식에서는 서로 연관된 3개의 객체가 텍스트 뷰에 표시할 문자열과 영역의 크기를 모두 관리해주기 때문에 간단한 코드로도 복잡한 레이아웃을 손쉽게 만들 수 있게 되었습니다.

textkit_multicolumn_result

Multi Device

앞에서 멀티 컬럼을 구현할 때, 텍스트 저장소에 하나의 레이아웃 메니저를 추가했습니다. 만약 아이폰과 아이패드에서 서로 다른 레이아웃으로 텍스트를 출력하고 싶거나 화면 출력용 레이아웃과 프린터 출력용 레이아웃을 구분하고 싶다면 텍스트 저장소에 필요한 수만큼 레이아웃 메니저를 추가하는 방법으로 구현할 수 있습니다.

textkit_multidevice_configuration

기본적으로 이러한 구성에서는 레이아웃 메니저가 하나의 텍스트 컨테이너와 연관되지만, 필요한 경우 다수의 컨테이너와 연관시키는 것도 얼마든지 가능합니다. 각 객체를 생성하고 연결하는 방식도 Code3에서 사용한 방식과 동일합니다.

 

Hit Testing

터치한 곳의 단어를 추출하는 기능도 아주 짧은 코드를 통해 구현할 수 있습니다.

먼저 4번 라인에서 터치의 위치를 구한 뒤, 5번 라인에서 터치한 위치에 있는 문자의 인덱스를 가져옵니다. 그런 다음 인덱스의 값을 통해 간단한 유효성 검사를 수행합니다. 여기에서는 인덱스 값이 전체 텍스트 길이 내에 있는 경우 유효한 인덱스로 판단하고 있습니다.

8번 라인에서는 NSString 클래스의 enumerateSubstringsInRange:options:usingBlock: 메소드를 통해서 텍스트 저장소의 있는 문자열을 단어 단위로 열거합니다. 9번 라인에서는 문자 인덱스가 각 단어의 범위에 속하는지 확인하고, 속하는 경우 경고창을 통해 터치된 단어를 보여주고 열거를 종료합니다.

 

(텍스트 킷 예제들은 iOS 7이 정식으로 출시되는 시점에 다시한번 업데이트 될 예정입니다.)

Filed under: Apple WWDC 2013, iOS