티스토리 뷰

반응형


안녕하세요 :) Zedd입니다.

요새 TextKit을 다루는 일이 생겼는데 아직 TextKit을 잘 모르니까 막 잘 모르겠고...암튼 왜 TextKit공부를 멈췄을까...라는 생각이 들었읍니다....

왜 멈췄었지?..바빴었나 

암튼 다시 공부하면 되니깐요

TextKit의 고수가 되겠어

근데 TextKit세션이 은근 몇개 없네여^_ㅠ 얼른 다 봐야지

대충 Advanced Text Layouts and Effects with Text Kit을 읽어봤는데, 가장 공부가 많이 될 것 같은 세션이네요.

iOS ) TextKit

iOS ) Typographical Concepts

도 참고하세요 XD


Advanced Text Layouts and Effects with Text Kit



TextKit글에서도 말했다 시피,

TextKit에는 앱의 Text를 표현하고, 사용자가 화면에서 볼 수 있는 글리프로 변환하는데 사용하는 3가지 클래스가 있습니다.



바로 요 3개죠.


먼저, 앱의 Text에 대한 backing store를 제공하는 Text Storage입니다.


이 NSTextStorage가 NSMutableAttributedString의 하위클래스인것을 잊지마세요.  

NSTextStorage라 하면 아 뭐야...뭔데 이게;;;라고 생각이 들지만 우리././? 암튼 저에게 익숙한 NSMutableAttributedString의 하위클래스라고 생각하면 아 뭔가 text의 attribute와 관련이 있겠구나..싶죠!!그쵸 뭔가 친숙해지죠?..

NSMutableAttributedString의 하위클래스이기 때문에, 모든 standard attributed string, key, value를 지원합니다. 

NSTextStorage는 다목적으로 사용 할 수 있기때문에, 사용자의 요구에 완벽하게 적합해야합니다.

하지만, NSTextStorage를 서브클래싱 할 필요가 발견되면, primitive attributed string과 mutable attributed string API를 모두 override해야합니다.

말 그대로 text"Storage"에요. 위에서도 말했다 시피 "앱의 Text에 대한 backing store를 제공"하는 거에요.

Text에 대한!!!!!!!!!!!!!

이제 좀 느낌이 오시나요?



그리고 NSLayoutManager!

NSTextStorage가 Text와 모든 유니코드 문자 및 attribute에 대한 backing store를 제어하는 것이라면, LayoutManager는 해당 Text를 화면의 글리프로 변환하는 객체입니다. (NSLayoutManager는 Text를 글리프로 변환하는 방법을 담당합니다.)

layout process를 override하려면, Delegate를 통해 할 수 있는데요. 예를들어, folding line 또는 고오급 텍스트 렌더링과 같은 고급 레이아웃 기술을 수행 할 수 있습니다. 


마지막으로 NSTextContainer.

NSTextContainer는 디스플레이에서, Text를 그릴 영역(area)를 나타냅니다.





NSTextContainer는 실제 drawing을 하는 객체는 아닙니다. 그건 textview가 함 ㅇㅇ

대신 TextContainer는 우리가 그려야 하는 geometry를 나타냅니다. TextKit에서 언급했다시피, exclusion path도 TextContainer를 이용한 거였죠. 

그냥 간단히 말하면 TextContainer는 Text를 그리는 한 영역(area)를 나타낸다!!!!!고 보면 됩니다.


그럼 우리 음..책을 생각해봐요.

왼쪽에 페이지가 있고 오른쪽에도 페이지가 있죠? 

== 왼쪽에 Text를 그리는 영역이 있고, 오른쪽에도 Text를 그리는 영역이 있죠?

== 왼쪽에 TextContainer가 있고, 오른쪽에도 TextContainer가 있죠?


!!!! 즉, 


TextContainer가 2개가 있으면 위와같이 여러 페이지가 있게 만들 수 있어요.

한마디로 말하자면, 여러 페이지 또는 여러 열(columns)을 표시하려는 경우, 추가 TextContainer가 필요합니다.



이건 columns이 2개있는거!! TextContainer가 2개인것 보이시죠?

왼쪽이 한 영역, 그리고 오른쪽도 한 영역이라고 보시면 됩니다.



자자 암튼 위에있는 3개가 모여 하나의 TextView를 만들게 되죠.



이렇게요 :)


자, 설명을 더 봅시다. 


NSLayoutManager는 NSTextContainer와 NSTextStorage를 조정/통제(orchestrates)하는 TextKit controller class입니다. 

그리고 Layout정보를 관리하고 저장합니다.

이 정보를 사용하여 NSLayoutManager는 Text를 측정하고 렌더링합니다.

NSLayoutManager는 열려있도록(open) 설계되었습니다.

그래서 TextKit객체가 사용하는 모든 정보를 앱에서 사용 할 수 있습니다. 



더 깊이 들어가기 전에 "Text Layout"이 무엇인지 다시 정리하고 가죠.

Text Layout...자 우리가 한번 생각해봅시다.

텍스트의 레이아웃이래요. 그럼 텍스트가 어디에 위치하고 그런건가...?? 레이아웃이니까?

맞습니다. Text Layout은 기본적으로 글리프(glyphs)와 위치(location)입니다.



그렇다면 글리프는?? 글리프는 저번에 다른글에서 설명했듯이 



이겁니다.

한 character의 그래픽 표현이에요.



Character에 font 가 들어간다? 그게 하나의 글리프에요.ㅇㅋ??


자..Text Layout정보의 기본 사항에 대해 봤습니다.

그럼 NSLayoutManager가 이 Layout 정보를 사용하여 수행 할 수 있는 몇가지 작업이 있어요.

위에서 뭐라고 그랬죠???!??!?

"NSLayoutManager는 Layout정보를 관리하고 저장합니다. 이 정보를 사용하여 NSLayoutManager는 Text를 측정하고 렌더링합니다."

야 내가 글리프 정보를 가지고 있어. 뭐 폰트 정보를 가지고 있다는 거겠죠? 해당 character랑?

그럼 그 폰트 정보에 뭐 size나..뭐 그런게 있겠죠? 


아 여러분 일단 iOS ) Typographical Concepts 이걸 읽고오면zzzzzz 조금 이해가 될거에요. 이제보니까 이 글에서 써놓은거를 또 이야기하고있네;;


아무튼 WWDC에서 말하는 걸 정리해보면, 

1. 전체 문자열의 size를 가져오면, 원하는대로 single line의 크기를 single glyph까지 지정 할 수 있어요.



2. TextKit에서 언급했듯이, touch로 character 또는 word를 hitTest 할 수 있는건 매우 자명합니다.




3. 또한 정확한 location을 얻고, 개별 glyph의 완벽한 위치...?(perfect location)를 수정하여 문서의 임의의 character범위(range)에서 사용자 정의 렌더링 또는 애니메이션을 추가 할 수 있습니다.

또한 모든 Text Layout정보, glyph 및 location을 사용하여 core graphic 시스템의 기능을 사용해 Text를 변형하고 애니메이션을 만들 수 있습니다.




아..글이 날라갂ㅆ네요 진짜 개빡친다 하

자..암튼 layout Manager에 저장된 glyph정보를 살펴봅시다.



자, 위 메소드를 사용하여 glyph정보에 접근 할 수 있다고 하는데요,

Swift버전을 보자면..


self.layoutManager.glyph(at: 0)


이거입니다. 자!! 근데 얘ㅖ 정의를 가봤더니..

위 두 메소드는 iOS9부터 soft deprecated되고 있다고 하네요. 대신 쓰라는 걸 써봅시다..


self.layoutManager.cgGlyph(at: 0)


자..이겁니당!!!

이 메소드의 리턴타입은 이름에 걸맞게 CGGlyph인데요 

자 근데 위에서 이 메소드를 사용하면 glyph정보에 접근 할 수 있다고 그랬잖아요?

CGGlyph하면 그럼 뭔가 glyph정보들이 모여있는..그런 느낌있잖아요 막 엄청 프로퍼티도 많고 막 그런거

그런걸 줄 알았는데 출력해보니까ㅓ 숫자가 나오는거에요!!!!

그래서 엥;;;이러고 확인해봤더니


CGGlyph == UInt16.....

ㄷㄷ


자 암튼 계ㅒ속 봅시다.

우리가 아까 


self.layoutManager.cgGlyph(at: 0)


여기서 index를 전달해줬었죠?

여기서 전달하는 index는 glyph index라는 점에 유의하라고 합니다.

NSTextStorage의 content에 접근하는데 사용하는 character index가 아니에요.

엥...glyph index? character index?


자, glyph index와 character index는 대게 동일하지만 항상 1:1로 매핑되지는 않는다고합니다.

ligatures(합자), truncation, hyphenation(하이픈)이 있기 때문이에요.


이게 truncation


이게 hyphenation


하지만 glyph를 원래 character 디렉토리에 매핑하는 일반적인 상황이 많아요.

따라서 NSLayoutManager는 각 glyph의 원래 character index를 추적(track)합니다. :)



characterIndexForGlyphAtIndex메소드를 사용하여 character index에 접근 할 수 있습니다. 


이건 glyph index에 접근하는 방법이겠죠?



NSLayoutManager로 작업 할 때, 이러한 메소드들 중 하나를 사용하여 glyph index 및 character index를 변환해야한다는 사실을 기억하세요.


이부분이 잘 이해가 안가네요.

glyph index와 character index가 항상 1:1매핑이 아니니까, 만약 glyph index 또는 character index를 가지고 반대의(?) 서로의 index를 가져오고 싶으면 메소드를 사용해라~~ 이런 것 같습니다.

아래 range이야기가 나온건 합자 때문이 아닐까 생각해봅니다 ㅋ.ㅋ


자. 위에서 glyph는 정보를 포함하고 있다고 했는데요, NSLayoutManager는 문자 레이아웃 정보, glyph 할당을 추적한다고 합니다.

Text Layout에는 일반적으로 Text Container, line 및 glyph location라는 세가지 요소가 있습니다.



NSLayoutManager는 Text container의 배열에 연결하고, glyph는 인덱스 0의 Text container시작 부분부터 채우게 됩니다.



textContainerForGlyphAtIndex effectiveRange메소드를 사용해, glyph와 연결된 TextContainer에 접근 할 수 있습니다. 오...

이 메소드를 사용해서 모든 TextContainer와 해당 glyph range에 애니메이션을 적용 할 수 있습니다. 


자 다음은 Line.

glyph가 TextContainer에 속해있는 것 처럼, glyph는 line안에 있습니다.

TextContainer는 text line으로 채워져있습니다.



그러나 exclusion path로 정의된 NSTextContainer의 geometrical shape(기하학적 모양) 때문에 더 큰 line을 여러조각으로 나눌 수 있습니다.



이렇게요. 

이러한 이유로 인해, 요소를 line fragement라고 합니다.



lineFragmentRectForGlyphAtIndex effectiveRange메소드를 사용하여 glyph의 line fragment에 접근 할 수 있습니다.

이 메소드는 line fragment의 직사각형 영역을 나타내는 CGRect를 반환합니다.


마지막으로 glyph location은 내부에 있는 line fragment rect를 기준으로 합니다.



(리턴값이 CGPoint죠? 말 그대로 location입니다. 위에서 line fragment의 rect를 superView로 본다는 뜻같네요.)


자..지금까지 API에 대해서 다뤘는데요, 레이아웃 정보 안에 있는 3가지 기하학적 요소(eometrical elements)간의 실제 관계를 설명할게요.

위에서 말한 TextContainer, line fragment, glyph location말이에요 :)


자..이렇게 TextContainer가 있습니다.



TextContainer는 자체 시스템이 있으며, 왼쪽 상단에서 시작됩니다.

TextContainer의 원점은 실제로 superView의 view좌표계 안에 있을 수 있습니다.



이렇게요. superView로 부터 (12, 6)만큼 떨어져있네요.


자, line fragment를 볼까요? line fragment는 위에서 말했듯이 CGRect로 표시됩니다. 



이렇게요. (파란 박스)

저 파란박스의 frame의 origin은 line fragment와 관련이 있죠.

TextContainer의 좌표시스템과 관련이 있습니다.

그럼 위 그림에서는 저 4번째 line fragment(CGRect)의 origin은 (0, 68)이네요. TextContainer가 자기의 superView라는거죠ㅕ.


자.. 더 들어가봅시다.



line fragment안에는 왼쪽 상단 모서리부터 시작하는 자체 좌표시스템이 있어요!


각 glyph들은 line fragment의 왼쪽 상단 모서리에서 시작하는 baseline origin에 있습니다.



자 이렇게 하고 몇가지 예제를 들어주는데..이건 넘어가도록 하겠습니다. 

아 하나만 해볼게요. 바로 TextView line number...저도 이거 때문에 조금 헤맸었는데요. 

어떻게 TextView같은 애한테 numberOfLines같은 프로퍼티가 없는 지 이해가 잘 안갑니다. 애플 도랐냐고 야 돌았냐고

하핫 아닙니다 ㅎ_ㅎ TextKit을 공부하게 만들기 위한 애플의 큰 그림 아닐까여ㅛ? 애플 짱~!@~!~~




 func numberOfLines() -> Int? {

        var numberOfLines = 0

        let layoutManger = self.textView.layoutManager

        let textContainer = self.textView.textContainer

        var glyphRange = NSRange(location: 0, length: 0)

        var lineRange = NSRange(location: 0, length: 0)

        var lastOriginY: CGFloat = -1.0

        var rect: CGRect?

        glyphRange = layoutManger.glyphRange(for: textContainer)

        while lineRange.location < NSMaxRange(glyphRange) {

            rect = layoutManger.lineFragmentRect(forGlyphAt: lineRange.location, effectiveRange: &lineRange)

            guard let rect = rect else { return nil }

            if ((rect.minY > lastOriginY) == true) { numberOfLines += 1 }

            lastOriginY = rect.minY

            lineRange.location = NSMaxRange(lineRange)

        }

        return numberOfLines

    }


자..프로퍼티가 엄청 많은데..이건 뭐 알아서 정리해주시구요 위 objc코드를 그냥 converting하느라..

암튼 테스트해보니 잘 나옵니다. 애플 짱~~!~!~


ㅈ ㅏ...그럼 마지막 섹션..

TextLayout을 커스터마이징 해볼겁니다.

NSLayoutManager는 정말 많은 delegate 인터페이스 set을 제공하는데요,

몇가지 인터페이스를 사용하면, 변경사항에 대한 알림(notification)을 받을 수 있습니다. 뭐..컨테이너의 레이아웃이 완료된 경우 등등이요.

그 중에 하나가 줄 간격(line spacing)을 재정의 할 수 있습니다.



Delegate객체는 모든 line fragment의 끝(end)에서 참조되며, 해당 Text와 연관된 paragraph style로 저장된 값을 재정의 하는 자체 line spacing을 제공 할 수 있습니다.

예를들어, paragraph style의 line spacing value를 사용하면 전체 paragraph에 대해 line spacing이 적용됩니다.

그래서 어케하냐고....그건 안나오내ㅔ;;;

암튼 Delegate에 있을 것 같네요 볼까요?


먼저, 


class ViewController: UIViewController, NSLayoutManagerDelegate {


내 viewController가 NSLayoutManagerDelegate를 채택하도록 해주시고,


self.textView.layoutManager.delegate = self


이렇게 해주고


func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { }


line spacing만 쳐도 이거 나옴.

암튼 



func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat {

        return CGFloat.random(in: 0 ..< 60)

    }


그냥...일단 써보고 싶어서 이렇게만 해줌

그러면ㅁ?!?!?


네....아무것도 안변하는 것도 알 수 있으며

Delegate메소드는 호출도 되지 않는 것을 볼 수 있습니다 ㅋ.ㅋ...

아니 


self.textView.layoutManager.delegate = self


이게 틀리다고? ?????/

ㅁ,......그래서 어카지 하다가 


let layoutManager = self.textView.layoutManager

layoutManager.delegate = self

self.textView.textContainer.replaceLayoutManager(layoutManager)


이렇게 해주니까 됨..

TextView에 있는 layoutManager를 꺼내서...거기서 self지정해주고 replaceLayoutManager...

아니 모가 다른데요 


암튼..

각 line line사이에 랜덤한 값이 들어가게 댐 

오옹 커서 크기 실화냐구


아 지금 발견한건데, 

위에처럼 layoutManager를 replace해주면, 딱 빌드하고 나서 위 그림처럼 딱 뜨거든요? 근데


self.textView.layoutManager.delegate = self


를 하면 빌드하고나서는 안뜨고, 









이러케...흠..왜 갑자기 GIF가 반복이 안되지..?;;;

암튼 replaceLayoutManager할 때는 처음 딱 빌드하면 저 모든 line fragment에 대해서 호출이 쫙 되는데, 그냥 self를 때려버리면 호출이 안되고 이렇게 뭐 변경될 때...불리네요., replaceLayoutManager도 뭐 변경될때마다 불리는건 똑같.ㅋ.ㅋ.


그리고 

soft line breaking!

기본적으로 유니코드 표준에서 제공하는 줄 바꿈 로직을 사용하므로 대부분의 경우에 충분하며 iOS에서 사용 할 수 있는 모든 언어ㅔ 대해 줄 바꿈 방식을 제공하고 있어요.

그러나 몇몇 경우에는 인쇄상의 필요에 따라 줄바꿈의 방법을 다르게 하고싶을 수 있죠.


아니 하는 법도 보통 알려주지 않나?????


 func layoutManager(_ layoutManager: NSLayoutManager, shouldBreakLineByWordBeforeCharacterAt charIndex: Int) -> Bool {


}


위 메소드인데요, 

제가 한번.. return true를 해보겠읍니다..



자..제가 charIndex를 출력하게 해놔서..저렇게 index들이 떴는데요!!! 자 저게 뭘까요!?!?

일단 두번 반복된 걸 볼 수 있음 ㅋ..

70~478까지만 일단 봅시다.

뭐 본다고 아는건 아니지만 제 추측으로는 줄바꿈이 된!!!!!!! 그 charIndex라고 할 수 있습니다.

ㅇㅋ?


아 괜히 로렘입숨으로 바꿨나zzz제가 한글로 테스트 하다가 아니 왜 안돼 하면서 한글이라..안되나..?하면서 바꿨거든요.

또 한글로 바꾸기 귀찮으니 일단 계속 하겠음


자..지금 우리가 return true를 한꺼구요 return false를 해보게쌈


??


일단 ㄴ맘에 안드는 점 1 :  순서가 엉망진창이다...

암튼 지금 제 추측으로는 저 나오는 charIndex는 line break가 될 수 있는 candidate의 느낌으로 보시면 될 것 같습니다. 

6은 Lorem ipsum < 이거임. 그니까 저기서 줄바꿈이 될 가능성이 있는것이죠.

암튼 저 완전 많이 나온 charIndex들은 candidate의 느낌이 오죠?


그렇다면 만약 


 func layoutManager(_ layoutManager: NSLayoutManager, shouldBreakLineByWordBeforeCharacterAt charIndex: Int) -> Bool {

        print(charIndex, "zedd")

        if charIndex == 6 { return true }

        return false

    }



ㅇㅇ내가 6일때 true를 return한다면...?? 

이러케 되는 것임. 

문서 안보고 김전일처럼 추측하면서 하는 것도 재밌네요. 문서는 알아서들 보샘 


자 다음!!



NSLayoutManager는 font자체 내에 저장된 character와 glyph사이의 매핑을 사용하는데요.

이 glyph 매핑을 override할 수 있습니다. 

훔,,이건 다양한 방법으로 할 수 있을 것 가튼..아직 LayoutManager로 하는 방법은 잘 모르겠네요.

암튼 Advanced Text Layouts and Effects with Text Kit은 이렇게 마무리 하려고 합니댜


TextKit정말 재밌으면서도 복잡하고...ㅠㅠ

그래도 오랜만에 주말에 공부하니까 상쾌하네요. 


아 벌써 12월이 됐네요.. 시간이 정말 빠른거 같ㅇ아요ㅠㅠㅠ


ㅠㅠㅠㅠ힝구

이것도 이제 지워야겠네요 

질문이 있으시다면 댓글에 달아주세요 XD

모두 즐거운 주말 되시길!!!! :)







반응형

'iOS' 카테고리의 다른 글

iOS ) available  (0) 2018.12.21
iOS ) Text Input Traits  (3) 2018.12.10
iOS ) Task Management - Scheduling - Timer  (0) 2018.11.30
iOS ) animateKeyframes, addKeyframe사용해보기  (0) 2018.11.23
iOS ) PhotoKit (2) - 미디어 가져오기  (13) 2018.11.09