[Objective-C] HTMLを使って文章をスタリングする


HTMLは文章の装飾やリンクなどの設定がとても手軽にできます。
その恩恵を受けつつ、iOSでも似たような挙動をさせることができます。

↓こんな感じ
![capture.png](https://qiita-image-store.s3.amazonaws.com/0/5616/80003cec-3eb7-506b-e38a-b96e9d99845a.png "capture.png")

```objc
UITextView *textView = [[UITextView alloc] init];

// ベースとなるHTMLテキスト
NSString *html = @"これは<a href=\"http://css-eblog.com/\"><strong>リンク</strong></a>の<strong>テスト</strong>です。これは<a href=\"http://css-eblog.com/\"><strong>リンク</strong></a>の<strong>テスト</strong>です。これは<a href=\"http://css-eblog.com/\"><strong>リンク</strong></a>の<strong>テスト</strong>です。これは<a href=\"http://css-eblog.com/\"><strong>リンク</strong></a>の<strong>テスト</strong>です。これは<a href=\"http://css-eblog.com/\"><strong>リンク</strong></a>の<strong>テスト</strong>です。";

// Attributeのオプション
NSDictionary *options = @{ NSDocumentTypeDocumentAttribute     : NSHTMLTextDocumentType,
                           NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding) };
                           
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithData:[html dataUsingEncoding:NSUTF8StringEncoding]
                                                                          options:options
                                                               documentAttributes:nil
                                                                            error:nil];
                                                                            
CGFloat baseLineHeight = 19.8;
NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init];
para.minimumLineHeight = baseLineHeight;
para.maximumLineHeight = baseLineHeight;
[text addAttribute:NSParagraphStyleAttributeName
             value:para
             range:NSMakeRange(0, text.length)];

textView.editable       = NO;
textView.attributedText = text;
CGFloat width       = self.view.bounds.size.width - 20;
CGSize rangeSize    = CGSizeMake(width, MAXFLOAT);
CGRect boundingRect = [text boundingRectWithSize:rangeSize
                                         options:NSStringDrawingUsesLineFragmentOrigin
                                         context:nil];

boundingRect.size.width  = width;
boundingRect.size.height += baseLineHeight; // line heightなどの余白を考慮して調整
boundingRect.origin.x    = 10;
boundingRect.origin.y    = 100;
textView.frame = boundingRect;
textView.backgroundColor = UIColor.redColor;
[self.view addSubview:textView];
```

普通にHTMLを解析して、それぞれ適切に変換してくれます。（リンクも！）
さらには`style`属性でのスタイリングもできるので重宝しそうです。
※ ただ、本機能はiOS7以降のものです。


**[追記]**
`NSForegroundColorAttributeName`を変更してもリンクの色が変更できなかったんですが、どうやら`UITextView`に`linkTextAttributes`というプロパティを通して設定するようです。

```objc
aTextView.linkTextAttributes = @{NSForegroundColorAttributeName:UIColor.redColor};
```


## リンクテキスト部分以外のタップイベントを透過的にさせる

上記のリンク機能などを使うと、`a`タグを使ってリンクを設定することができました。
が、リンクを有効にするとリンクされていないテキストもタップを検知するようになり、結果としてUITableViweCellなどに置いた際に、セルが反応しない、という問題が出てしまいます。

それを回避する方法としてUITextViewの`hitTest:withEvent:`メソッドを使ってタップの検出をスキップするかをハンドリングすることが出来ます。

具体的には以下のようにします。

```objc
// リンクテキスト以外のタッチイベントを透過させる
- (UIView *)hitTest:(CGPoint)point
          withEvent:(UIEvent *)event
{
	// Inset分位置を補正
	point.x -= self.textContainerInset.left;
	point.y -= self.textContainerInset.top;

	// タッチ位置から該当テキストのindexを得る
	NSInteger index = [self.layoutManager characterIndexForPoint:point inTextContainer:self.textContainer fractionOfDistanceBetweenInsertionPoints:NULL];
	
	// タッチ位置にある文字と同種の属性が含まれる文字列の範囲（HTMLのタグの影響範囲のような感じ？）のポインタ
	NSRange effectiveRange = NSMakeRange(0, 0);
	
	// タッチ位置の文字インデックスの属性と、その属性範囲を取得
	NSDictionary *attributes = [self.textStorage attributesAtIndex:index effectiveRange:&effectiveRange];
	
	// 属性がリンクテキストでなかった場合はスルー（イベントを透過するには`nil`を返す）
	if (!attributes[NSLinkAttributeName]) {
		return nil;
	}
	
    __block BOOL isLink = NO;
    
    // 文字列インデックスからグリフのインデックスを得る
    NSInteger glyphIndex = [self.layoutManager glyphIndexForCharacterAtIndex:index];
    
    // グリフのrangeから該当文字列の矩形を得る？（ちょっとここは分かりません）
    [self.layoutManger enumerateLineFragmentsForGlyphRange:NSMakeRange(glyphIndex, 1) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
    	// タッチ位置がusedRectの中にある＝リンクテキストをタップした
    	if (CGRectContainsPoint(usedRect, p)) {
    		isLink = YES;
	       *stop = YES;
    	}
    }];
    
    // リンクテキストをタップしていたら自身を、そうでない場合はnilを返す
    return isLink ? self : nil;
}
```