Search
Duplicate

UITapGestureRecognizer를 사용해서 UILabel 안에 있는 문장을 끄집어내자

 사건의 발단

단락 별로 글을 볼 수 있어야 해서 TableView 안에 Cell를 만들어서 단락별로 글이 들어가도록 만듦
해야하는 기능 : 해당 단락을 눌렀을 때 해당 단락이 하이라이트 되어야 한다.
// MARK: - UITableViewDelegate extension MainSentenceViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let cell = tableView.cellForRow(at: indexPath) as? MainContentParagraphTableViewCell else { return } cell.isSelected = true tableView.scrollToRow(at: indexPath, at: .top, animated: true) } func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { guard let cell = tableView.cellForRow(at: indexPath) as? MainContentParagraphTableViewCell else { return } cell.isSelected = false } }
Swift
복사
누르면 isSelected가 true가 되도록 구현
isSelected 코드
override var isSelected: Bool { willSet { self.setSelected(newValue, animated: false) } } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) let paragraphType: ParagraphType = selected ? .highlighted : .original self.setupParagraphStyle(to: paragraphType) }
Swift
복사
setupParagraphStyle
private func setupParagraphStyle(to paragraphType: ParagraphType) { self.captionLabel.font = TextStyle.caption(paragraphType).font self.contentLabel.font = TextStyle.content(paragraphType).font self.captionLabel.textColor = paragraphType.textColor self.contentLabel.textColor = paragraphType.textColor self.contentLabel.isUserInteractionEnabled = paragraphType.isUserInteractionEnabled }
Swift
복사
현재 Cell 내부에 contentLabel에 걸려있는 TapGesture가 있습니다.
TapGesture가 highlight 상태에만 걸리도록 isUserInteractionEnabled 를 true, false 시켜서 컨트롤하고 있습니다.
original 상태인데 cell를 누른다 → cell이 하이라이트 된다.
highlight 상태인데 cell를 누른다 → contentLabel에 gesture가 걸린다.
처음 해보는 것이라서 UILabel를 어떤 식으로 건드려야 할 지 감이 안잡혀서 구글링을 시도해보았고, subText를 사용해서 rect의 영역을 알아내는 방식과 Tab했을 때 UILabel의 몇 번째 index를 터치했는지 알아내는 방식을 찾아냈다.
두 방식 모두 UILabel의 Extension 내부의 함수로 만들었다.

 첫 번째 도전(subText를 사용해서 rect의 영역을 알아내는 방식)

func boundingRectForCharacterRange(subText: String) -> CGRect? { guard let attributedText = attributedText else { return nil } guard let text = self.text else { return nil } // 전체 텍스트(text)에서 subText만큼의 range를 구합니다. guard let subRange = text.range(of: subText) else { return nil } let range = NSRange(subRange, in: text) // attributedText를 기반으로 한 NSTextStorage를 선언하고 NSLayoutManager를 추가합니다. let layoutManager = NSLayoutManager() let textStorage = NSTextStorage(attributedString: attributedText) textStorage.addLayoutManager(layoutManager) // instrinsicContentSize를 기반으로 NSTextContainer를 선언하고 let textContainer = NSTextContainer(size: intrinsicContentSize) // 정확한 CGRect를 구해야하므로 padding 값은 0을 줍니다. textContainer.lineFragmentPadding = 0.0 // layoutManager에 추가합니다. layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() // 주어진 범위(rage)에 대한 실질적인 glyphRange를 구합니다. layoutManager.characterRange( forGlyphRange: range, actualGlyphRange: &glyphRange ) // textContainer 내의 지정된 glyphRange에 대한 CGRect 값을 반환합니다. return layoutManager.boundingRect( forGlyphRange: glyphRange, in: textContainer ) }
Swift
복사
해당 방식의 문제라면, CGRect으로 영역을 잡기 때문에 원하지 않는 글을 사용자가 받을 수 있게 됩니다.
let subTextRect = self.contentLabel.boundingRectForCharacterRange(subText: "승합차는 일반 택시보다 크고 마을버스보다 작은 차종을 말합니다.") let rect = UIView(frame: subTextRect!) rect.backgroundColor = .red self.contentView.addSubview(rect)
Swift
복사
줄바꿈이 있는 글을 눌렀을 시에 이렇게 네모나게 원하지 않는 글까지 잡히게 된다.
“차량공유 앱 서비스입니다.”와 “2018년 10월 ‘타” 라는 부분을 눌러도 "승합차는 일반 택시보다 크고 마을버스보다 작은 차종을 말합니다."를 누른 것으로 판단할 수 있게 됨

 두 번째 도전(Tab했을 때 UILabel의 몇 번째 index를 터치했는지 알아내는 방식)

해당 위치의 텍스트 index를 받아서 해당 문장을 계산 해봐야겠다..
func textIndex(at point: CGPoint) -> Int? { guard let attributedText = attributedText else { return nil } let layoutManager = NSLayoutManager() let textContainer = NSTextContainer(size: self.bounds.size) let textStorage = NSTextStorage(attributedString: attributedText) textStorage.addLayoutManager(layoutManager) textContainer.lineFragmentPadding = 0.0 layoutManager.addTextContainer(textContainer) var textOffset = CGPoint.zero // 정확한 자체(glyph)의 범위를 구하고 그 범위의 CGRect 값을 구합니다. let range = layoutManager.glyphRange(for: textContainer) let textBounds = layoutManager.boundingRect( forGlyphRange: range, in: textContainer ) // textOffset.x가 패딩을 제외한 부분부터 시작하도록 합니다. let paddingWidth = (self.bounds.size.width - textBounds.size.width) / 2 if paddingWidth > 0 { textOffset.x = paddingWidth } // 눌려진 정확한 포인트를 구합니다. let newPoint = CGPoint( x: point.x - textOffset.x, y: point.y - textOffset.y ) // textContainer내에서 newPoint 위치의 glyph index를 반환합니다 return layoutManager.glyphIndex(for: newPoint, in: textContainer) }
Swift
복사
@objc private func didTappedView(_ gestureRecognizer: UITapGestureRecognizer) { guard gestureRecognizer.state == UIGestureRecognizer.State.recognized else { return } let gesturePoint = gestureRecognizer.location(in: gestureRecognizer.view) guard let selectedIndex = self.contentLabel.textIndex(at: gesturePoint) else { return } print(selectedIndex) }
Swift
복사
코드를 이렇게 짰을 때 위의 영상처럼 나오게 됩니다.
UITapGestureRecognizer 에 있는 location(in: _) 메소드를 사용해서 gesture가 걸려있는 view 내부에 point를 가져옵니다.
그리고 그 point에 해당하는 text index를 가져옵니다.
index를 가져오는 건 알았는데, 이 index를 가져와서 해당 글씨가 포함된 문장을 가져오는 건 어떻게 하면 될까 고민하던 와중에, 저 코드에서 애초에 문장을 가져올 수 있게 한다면 더 좋지 않을까 라는 생각이 듦
저 코드를 내가 원하는대로 만들기 위해서는 코드 분석을 해야할 필요가 있어보인다.

 코드를 분석해보자

 NSLayoutManager 누구시죠

텍스트 문자의 레이아웃과 표시를 조정하는 개체입니다.
텍스트 문자의 layout과 display를 조정하는 방식
NSLayoutManager → 유니코드 문자 코드를 글리프에 매핑 → NSTextContainer 개체에 글리프 설정 → NSTextView 개체에 표시
특징
텍스트 뷰 객체 조정, 문단 스타일 편집을 위한 NSRulerView 인스턴스 지원하는 텍스트 뷰에 서비스 제공(macOS), 글리프에 속하지 않는 텍스트 속성(밑줄, 가로줄)의 레이아웃 레이아웃 및 표시를 처리합니다.
사용자는 추가적인 text attribute를 다루기 위해 NSLayoutManager의 subclass를 만들 수 있어요.

Thread Safety of NSLayoutManager

Noncontiguous Layout(정리 필요)

 NSTextContainer 누구시죠

텍스트 레이아웃이 발생하는 영역입니다.
NSLayoutManager는 NSTextContainer를 사용하여 줄 바꿈 위치, 텍스트 레이아웃 등을 결정합니다.
NSTextContainer 객체는 일반적으로 직사각형 영역을 정의하지만 텍스트 컨테이너 내부에 exclusion path를 정의하여 텍스트가 flow하지 않는 영역을 만들 수 잇습니다.
subclass를 사용해서 원형 영역, 구멍이 있는 영역 또는 그래픽과 함께 flow하는 영역과 같은 직사각형이 아닌 영역이 있는 Text Container를 만들 수 있습니다.
앱이 한 번에 하나의 스레드에서만 액세스를 보장하는 스레드 이외의 스레드에서 NSTextContainer, NSLayoutManager 및 NSTextStorage 클래스의 인스턴스에 액세스할 수 있습니다.

 깨달음

설명을 통해서 일반적인 직사각형 영역이 아닌 텍스트 컨테이너를 만들 수 있다는 걸 알게 되었음
You can also subclass to create text containers with nonrectangular regions, such as circular regions, regions with holes in them, or regions that flow alongside graphics. 하위 클래스를 사용하여 원형 영역, 구멍이 있는 영역 또는 그래픽과 함께 흐르는 영역과 같이 직사각형이 아닌 영역이 있는 텍스트 컨테이너를 만들 수 있습니다.
글리프가 설정되는 NSTextContainer에서 이 부분을 관리하고 있다.
NSLayoutManager → 유니코드 문자 코드를 글리프에 매핑 → NSTextContainer 개체에 글리프 설정(이 부분에서 직사각형이 아닌 모양으로 설정해주면 되지 않을까?) → NSTextView 개체에 표시
찾아보니 Container Shape를 정의하는데 도움을 주는 인스턴스 프로퍼티들이 존재.
두 번째 방식으로 하려고 했는데 이렇게 모양을 만들 수 있다면 첫 번째 방식 + Rectangle 형태가 아니게 만들면 되지 않을까 라는 생각이 듦

 첫 번째 방식 코드를 뜯어보기

func boundingRectForCharacterRange(subText: String) -> CGRect? { guard let attributedText = attributedText else { return nil } guard let text = self.text else { return nil } // 전체 텍스트(text)에서 subText만큼의 range를 구합니다. guard let subRange = text.range(of: subText) else { return nil } let range = NSRange(subRange, in: text) // attributedText를 기반으로 한 NSTextStorage를 선언하고 NSLayoutManager를 추가합니다. let layoutManager = NSLayoutManager() let textStorage = NSTextStorage(attributedString: attributedText) textStorage.addLayoutManager(layoutManager) // instrinsicContentSize를 기반으로 NSTextContainer를 선언하고 let textContainer = NSTextContainer(size: intrinsicContentSize) // 정확한 CGRect를 구해야하므로 padding 값은 0을 줍니다. textContainer.lineFragmentPadding = 0.0 // layoutManager에 추가합니다. layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() // 주어진 범위(rage)에 대한 실질적인 glyphRange를 구합니다. layoutManager.characterRange( forGlyphRange: range, actualGlyphRange: &glyphRange ) // textContainer 내의 지정된 glyphRange에 대한 CGRect 값을 반환합니다. return layoutManager.boundingRect( forGlyphRange: glyphRange, in: textContainer ) }
Swift
복사
전체 코드
텍스트 문자의 layout과 display를 조정하는 방식
NSLayoutManager → 유니코드 문자 코드를 글리프에 매핑 → NSTextContainer 개체에 글리프 설정 → NSTextView 개체에 표시
(1)
일단 attributedText와 text를 사용해야 하기 때문에 option 바인딩을 진행해준다.
UILabel의 Extension 내부에 있는 함수이기때문에 attributedText를 따른 선언없이 사용가능하다.
guard let attributedText = attributedText else { return nil } guard let text = self.text else { return nil } // 전체 텍스트(text)에서 subText만큼의 range를 구합니다. guard let subRange = text.range(of: subText) else { return nil } let range = NSRange(subRange, in: text)
Swift
복사
전체 텍스트에서 subText만큼의 range를 구한다. 그리고 NSRange를 통해서 해당 range를 text 내부에서 구한다.
(2)
// attributedText를 기반으로 한 NSTextStorage를 선언하고 NSLayoutManager를 추가합니다. let layoutManager = NSLayoutManager() let textStorage = NSTextStorage(attributedString: attributedText) textStorage.addLayoutManager(layoutManager)
Swift
복사
잠만 이건 또 누구신가

 NSTextStorage 누구시죠

시스템에서 관리하는 텍스트를 포함하는 TextKit의 기본 저장 메커니즘입니다.
NSLayoutManager 집합을 관리하기 위한 동작을 추가하는 NSMutableAttributedString의 semi-concrete subclass 입니다.
NSMutableAttributedString
TextStorage 객체는 layout manager에게 character 또는 attributes의 변경 사항을 알려줘요. 그러면 layout manager가 필요에 따라서 텍스트를 redisplay 시켜줍니다.
앱의 any thread에서 Text Storage 객체에 접근할 수 있지만 앱은 한 번에 하나의 스레드에서만 액세스하는 것을 보장해야 합니다.
characters, words, pragraphs 속성을 사용하는 것은 text storage를 조작하는 비효율적인 방법이에요. 이런 attribute에 접근하려면 많은 객체가 생성되어야 하기 때문입니다.
대신 NSMutableAttributedString, NSAttributedString, NSMutableStringNSString에서 정의한 텍스트 액세스 메서드를 사용하여 character-level 조작을 수행합니다.

Subclassing Notes

그렇다면!
NSTextStorage에 대한 설명에 따르면 TextStorage 객체가 TextStorage에 등록된 layout manager에게 character, attribute 변경 사항을 알려주는 걸 알 수 있다.
현재 TextStorage에 들어가 있는 Text는 attributedText이다. 해당 Text에서 변경 사항이 생기면 textStorage에 add되어 있는 layoutManager에 변경 사항이 알려지게 되는 것.
(3)
// instrinsicContentSize를 기반으로 NSTextContainer를 선언하고 let textContainer = NSTextContainer(size: intrinsicContentSize) // 정확한 CGRect를 구해야하므로 padding 값은 0을 줍니다. textContainer.lineFragmentPadding = 0.0 // layoutManager에 추가합니다. layoutManager.addTextContainer(textContainer)
Swift
복사
NSTextContainer는 텍스트 레이아웃이 발생하는 영역이다.
Text Container의 bounding rectangle의 크기를 결정하는 size를 intrinsicContentSize로 설정한다. 본질적인 Content Size를 Container 사이즈로 지정해준 것. UILabel은 내부에 있는 콘텐츠(text)를 기반으로 본질적인 Content Size를 가지고 있다. 따라서 저런 식으로 지정해줄 수 있는 것.
설정해준 textContainer를 layoutManager의 TextContainer로 넣어준다.
(4)
var glyphRange = NSRange() // 주어진 범위(range)에 대한 실질적인 glyphRange를 구합니다. layoutManager.characterRange( forGlyphRange: range, actualGlyphRange: &glyphRange )
Swift
복사
characterRange는 지정한 글리프 범위의 글리프에 해당하는 문자 범위를 반환합니다.
forGlyphRange : 문자 범위를 반환할 글리프 범위
autalGlyphRange : NULL이 아니라면 출력시 반환된 문자 범위에서 생성된 전체 글리프 범위를 가리켜요. 이 범위는 요청된 글리프 범위와 동일하거나 약간 클 수 있어요.
ex) Text Storage가 문자 “Ö” 를 포함하고 있고 글리프 캐시에 두 개의 atomic한 글리프 “O” 와 “¨”가 포함되어 있고, 글리프 범위가 첫 번째 또는 두 번째 글리프만 포함되어 있다면, actualGlyphRange는 두 글리프를 모두 둘러싸도록 설정됩니다.
characterRange는 glyphRange가 0인 경우, resulting character range가 0이 나오고 actualGlyphRange도 0입니다. glyphRange가 텍스트 길이를 초과하는 경우에는 메서드가 결과를 텍스트의 문자 수만큼 잘라냅니다.
noncontiguous layout가 활성화되지 않은 경우에는 해당 메서드가 returned range의 끝을 포함하여 모든 문자에 대해 글리프를 생성합니다.
위에서 let range = NSRange(subRange, in: text), range는 text안에서 subRange(subText가 포함된 range)를 나타내는 값이었습니다. 따라서 forGlyphRange에는 subRange가 포함된 range가 들어가고 이 값을 토대로 지정한 글리프 범위의 글리프에 해당하는 문자 범위를 반환할 겁니다.
(5)
// textContainer 내의 지정된 glyphRange에 대한 CGRect 값을 반환합니다. return layoutManager.boundingRect( forGlyphRange: glyphRange, in: textContainer )
Swift
복사
boundingRect는 컨테이너에서 지정된 글리프에 대한 bound에 있는 사각형을 반환합니다.
forGlyphRange : 문자 범위를 반환할 글리프 범위
in : 글리프가 배치되는 텍스트 컨테이너
지정한 글리프 범위에 대해 지정된 Text Container에 그려진 모든 글리프와 기타 마크를 둘러싸는 bounding rectangle를 반환합니다.
line fragment rectangle 외부에 그리는 글리프와 밑줄과 같은 텍스트 속성이 포함됩니다.
range는 bounding rectangle를 계산하기 전에 컨테이너의 범위와 교차됩니다.
이 방법은 글리프 범위가 변경될 때 무효화 및 다시 그리기를 위해 글리프 범위를 display rectangle로 변환하는 데 사용할 수 있어요.
bounding rectangle은 항상 container coordinator에 있습니다.
필요한 경우 글리프 생성 및 레이아웃을 수행
위에 있는 코드에서 글리프에 해당하는 문자 범위를 glyphRange에 담았습니다. 이번에는 textContainer 내부에 지정된 glyphRange에 대해서 CGRect를 반환받습니다. 해당 CGRect 값은 지정한 글리프 범위에 대해 지정된 Text Container 내에 그려진 모든 글리프와 마크를 둘러싸는 bounding rectangle를 반환할 겁니다.
 코드 뜯어보기 정리
(1) 현재 UILabel에 대한 attributedText와 text를 가져옵니다. 전체 텍스트에서 매개변수로 넣은 subText 만큼의 subRange를 얻고 해당 Range 값을 NSRange로 변환합니다.
(2) 텍스트 character의 layout과 display를 조정하는 NSLayoutManager 인스턴스를 생성합니다. 해당 인스턴스는 유니코드 문자 코드를 글리프에 매핑하고 NSTextContainer 객체에 글리프를 설정하고 이를 NSTextView 객체에 표시하는 객체입니다. NSLayoutManager와 함께 NSTextStorage 인스턴스도 생성해줍니다. NSTextStorage는 NSLayoutManager 객체들을 관리하기 위한 동작을 추가하는 클래스 입니다. 우리는 attributedText를 넣은 NSTextStorage를 하나 생성합니다. 그리고 TextStorage에 만들어둔 LayoutManager를 넣습니다. add된 LayoutManager는 해당 attributedText에서 변경 사항이 생기면 해당 변경 사항이 생겼다는 걸 Text Storage를 통해서 듣습니다. 그러면 layout manager가 필요에 따라서 텍스트를 redisplay 시켜줍니다.
(3) TextContainer를 instrinsicContentSize 사이즈로 하나 만들어두고 정확한 CGRect를 구해야하기 때문에 padding를 0으로 설정해줍니다. 그리고 layout manager가 텍스트를 정렬할 때 사용하기 위해서 Container를 넣어줍니다. NSLayoutManager가 후에 NSTextContainer 개체에 글리프를 설정해줄겁니다.
(4) 위에서 우리가 원하는 텍스트 문자를 받을 준비를 끝냈으니 원하는 범위의 텍스트 CGRect를 받을 준비를 시작합니다. layoutManager에 있는 characterRange를 사용해서 아까 계산해둔 subText 만큼의 subRange에 해당하는 문자 범위로 반환합니다.
(5) layoutManager에 있는 boundingRect를 사용해서 textContainer 내부에 지정된 glyphRange에 대한 CGRect를 반환받습니다. 해당 CGRect 값은 지정한 글리프 범위에 대해 지정한 Text Container 내에 그려진 모든 글리프와 마크를 둘러싸는 bounding rectangle를 의미합니다.
세 줄 요약
subText range 생성
해당 range에 대한 glyph range를 Text Container 내부에 생성
이에 대한 CGRect 반환
이 코드에서 내가 수정할 수 있는 부분은 어떤 곳일까?
func boundingRectForCharacterRange(subText: String) -> CGRect? { guard let attributedText = attributedText else { return nil } guard let text = self.text else { return nil } guard let subRange = text.range(of: subText) else { return nil } let range = NSRange(subRange, in: text) let layoutManager = NSLayoutManager() let textStorage = NSTextStorage(attributedString: attributedText) let textContainer = NSTextContainer(size: intrinsicContentSize) textContainer.lineFragmentPadding = 0.0 textStorage.addLayoutManager(layoutManager) layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() layoutManager.characterRange( forGlyphRange: range, actualGlyphRange: &glyphRange ) return layoutManager.boundingRect( forGlyphRange: glyphRange, in: textContainer ) }
Swift
복사
전체 코드
boundingRect를 생성하는 부분에서 내가 원하는대로 Rectangle 영역을 변경할 수 있을 거 같은데!
NSTextContainer에서 직사각형 영역이 아닌 Text Container를 만들어보자.

 뚝딱뚝딱 거려보기

Text Container의 Rectangle 영역을 어떻게 자르지?
Text Container의 exclusionPaths라는 [UIBezierPath] 타입을 찾았다. 뭔가 여기에 exclusion하고 싶은 path를 넣으면 텍스트가 표시되지 않는 영역이라는 걸 알고서 Rectangle 형식으로 안나오지 않을까?
→ 하다보니 이 기능을 구현하려면 매번 subText를 넣어줘야 하는데.. 이건 나중에 잡힌 영역을 나타내는 기능을 구현할 때 맞을 듯 하다.!!!!
→ index로 가져와서 문장을 가져오고 해당 문장을 subText로 가져와서 잡힌 영역을 그려주는 거
그럼 index를 가져와서 문장으로 return하는 게 맞을 거 같다.
→ index를 가져와서 해당 단어 앞뒤로 for문을 돌려서 하나의 문장을 만들어내야 하나?
→ 너무 어려운데? 로직이 너무 복잡해!
< 최종 방식 >
처음 paragraph에 들어오는 단락을 문장 단위로 쪼개자! 그리고 각각의 문장의 시작 index, 끝 index range를 저장해두고 그 안에 포함되는 문장도 저장해두자 → Dictionary 형식으로 저장해보자. range를 key 형태로 만들 수 있을까?
그러면 index를 가져와서 저 안에 넣으면 range 안에 contain된다! 라고 하면 해당 문장을 가져오기
→ 해당 문장 가져와서 오른쪽 뷰에 넣기
→ 해당 문장으로 첫 번째 방식으로 rectangle 만들기
대성공~!~!

 뷰 기능 다듬기

let subTextRect = self.contentLabel.boundingRectForCharacterRange(subText: "글자를 새긴 차") let rect = UIView(frame: subTextRect!) rect.backgroundColor = .red self.contentView.addSubview(rect)
Swift
복사
func boundingRectForCharacterRange(subText: String) -> CGRect? { guard let attributedText = attributedText else { return nil } guard let text = self.text else { return nil } // 전체 텍스트(text)에서 subText만큼의 range를 구합니다. guard let subRange = text.range(of: subText) else { return nil } let range = NSRange(subRange, in: text) // attributedText를 기반으로 한 NSTextStorage를 선언하고 NSLayoutManager를 추가합니다. let layoutManager = NSLayoutManager() let textStorage = NSTextStorage(attributedString: attributedText) textStorage.addLayoutManager(layoutManager) // instrinsicContentSize를 기반으로 NSTextContainer를 선언하고 let textContainer = NSTextContainer(size: intrinsicContentSize) // 정확한 CGRect를 구해야하므로 padding 값은 0을 줍니다. textContainer.lineFragmentPadding = 0.0 // layoutManager에 추가합니다. layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() // 주어진 범위(rage)에 대한 실질적인 glyphRange를 구합니다. layoutManager.characterRange( forGlyphRange: range, actualGlyphRange: &glyphRange ) // textContainer 내의 지정된 glyphRange에 대한 CGRect 값을 반환합니다. return layoutManager.boundingRect( forGlyphRange: glyphRange, in: textContainer ) }
Swift
복사

 나아가기

TextKit에 대해서 더 공부해봐야겠다. WWDC 영상도 꽤 있고,
Zedd님 블로그에 가니, Advanced Text Layouts and Effects with Text Kit 이라는 글을 읽은 흔적이 있어서 나도 한 번 봐야겠음.
글을 읽고나면 여기다가 글을 남기겠음!