tl;dr:テキストのパスをヒット テストできます。Gist はこちらから入手できます。
私が使用するアプローチは、タップ ポイントがテキストのパス内にあるかどうかを確認することです。詳細に入る前に、手順の概要を説明します。
- サブクラス UILabel
- Core Text を使用して、テキストの CGPath を取得します
pointInside:withEvent:
ポイントを内側と見なすかどうかを決定できるようにオーバーライドします。
- タップ ジェスチャ レコグナイザーなどの「通常の」タッチ処理を使用して、いつヒットしたかを確認します。
このアプローチの大きな利点は、フォントに正確に従うことと、以下に示すようにパスを変更して「ヒット可能な」領域を拡大できることです。黒とオレンジの両方の部分がタップ可能ですが、ラベルに描画されるのは黒の部分だけです。
サブクラス UILabel
UILabel
呼び出されたのサブクラスを作成TextHitTestingLabel
し、テキスト パスのプライベート プロパティを追加しました。
@interface TextHitTestingLabel (/*Private stuff*/)
@property (assign) CGPathRef textPath;
@end
iOS ラベルは atext
または an のいずれかを持つことができるattributedText
ため、これらのメソッドをサブクラス化し、テキスト パスを更新するメソッドを呼び出すようにしました。
- (void)setText:(NSString *)text {
[super setText:text];
[self textChanged];
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
[super setAttributedText:attributedText];
[self textChanged];
}
また、NIB/ストーリーボードからラベルを作成することもできます。この場合、テキストはすぐに設定されます。その場合、awake from nib の最初のテキストをチェックします。
- (void)awakeFromNib {
[self textChanged];
}
Core Text を使用してテキストのパスを取得します
Core Text は、テキストのレンダリングを完全に制御できる低レベルのフレームワークです。プロジェクトに追加CoreText.framework
してファイルにインポートする必要があります
#import <CoreText/CoreText.h>
内部で最初に行うことtextChanged
は、テキストを取得することです。iOS 6 以前かどうかによっては、属性付きのテキストも確認する必要があります。ラベルには、これらのうちの 1 つのみが含まれます。
// Get the text
NSAttributedString *attributedString = nil;
if ([self respondsToSelector:@selector(attributedText)]) { // Available in iOS 6
attributedString = self.attributedText;
}
if (!attributedString) { // Either earlier than iOS6 or the `text` property was set instead of `attributedText`
attributedString = [[NSAttributedString alloc] initWithString:self.text
attributes:@{NSFontAttributeName: self.font}];
}
次に、すべての文字グリフの新しい可変パスを作成します。
// Create a mutable path for the paths of all the letters.
CGMutablePathRef letters = CGPathCreateMutable();
コアテキスト「魔法」
Core Text は、テキスト行とグリフおよびグリフ ランで動作します。たとえば、「Hello」というテキストがあり、「Hello」のような属性を持っているとします(わかりやすくするためにスペースが追加されています)。次に、2 つのグリフ ラン (1 つはボールド、もう 1 つは通常) を持つ 1 行のテキストになります。最初のグリフ ランには 3 つのグリフが含まれ、2 番目のグリフ ランには 2 つのグリフが含まれます。
すべてのグリフ ランとそのグリフを列挙し、 でパスを取得しますCTFontCreatePathForGlyph()
。次に、個々のグリフ パスが可変パスに追加されます。
// Create a line from the attributed string and get glyph runs from that line
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFArrayRef runArray = CTLineGetGlyphRuns(line);
// A line with more then one font, style, size etc will have multiple fonts.
// "Hello" formatted as " *Hel* lo " (spaces added for clarity) is two glyph
// runs: one italics and one regular. The first run contains 3 glyphs and the
// second run contains 2 glyphs.
// Note that " He *ll* o " is 3 runs even though "He" and "o" have the same font.
for (CFIndex runIndex = 0; runIndex < CFArrayGetCount(runArray); runIndex++)
{
// Get the font for this glyph run.
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
// This glyph run contains one or more glyphs (letters etc.)
for (CFIndex runGlyphIndex = 0; runGlyphIndex < CTRunGetGlyphCount(run); runGlyphIndex++)
{
// Read the glyph itself and it position from the glyph run.
CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
CGGlyph glyph;
CGPoint position;
CTRunGetGlyphs(run, glyphRange, &glyph);
CTRunGetPositions(run, glyphRange, &position);
// Create a CGPath for the outline of the glyph
CGPathRef letter = CTFontCreatePathForGlyph(runFont, glyph, NULL);
// Translate it to its position.
CGAffineTransform t = CGAffineTransformMakeTranslation(position.x, position.y);
// Add the glyph to the
CGPathAddPath(letters, &t, letter);
CGPathRelease(letter);
}
}
CFRelease(line);
コア テキストの座標系は、通常の UIView 座標系とは上下が逆になっているため、画面に表示されるものと一致するようにパスを反転させます。
// Transform the path to not be upside down
CGAffineTransform t = CGAffineTransformMakeScale(1, -1); // flip 1
CGSize pathSize = CGPathGetBoundingBox(letters).size;
t = CGAffineTransformTranslate(t, 0, -pathSize.height); // move down
// Create the final path by applying the transform
CGPathRef finalPath = CGPathCreateMutableCopyByTransformingPath(letters, &t);
// Clean up all the unused path
CGPathRelease(letters);
self.textPath = finalPath;
これで、ラベルのテキストの完全な CGPath ができました。
オーバーライドpointInside:withEvent:
ラベルがそれ自体の内部と見なすポイントをカスタマイズするには、内部のポイントをオーバーライドし、ポイントがテキスト パスの内部にあるかどうかをチェックします。UIKit の他の部分は、ヒット テストのためにこのメソッドを呼び出します。
// Override -pointInside:withEvent to determine that ourselves.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// Check if the points is inside the text path.
return CGPathContainsPoint(self.textPath, NULL, point, NO);
}
通常のタッチ操作
これで、すべてが通常のタッチ処理で動作するようにセットアップされました。NIB のラベルにタップ認識エンジンを追加し、View Controller のメソッドに接続しました。
- (IBAction)labelWasTouched:(UITapGestureRecognizer *)sender {
NSLog(@"LABEL!");
}
それだけです。ここまでスクロールして、さまざまなコードを貼り付けたくない場合は、ダウンロードして使用できる Gist に .m ファイル全体があります。
ほとんどのフォントは、タッチの精度 (44px) に比べて非常に薄いため、タッチが「ミス」と見なされると、ユーザーは非常に不満を感じる可能性が高いことに注意してください。そうは言っても、ハッピーコーディング!
アップデート:
ユーザーに少し優しくするために、ヒット テストに使用するテキスト パスをストロークできます。これにより、タップ可能なヒット領域が大きくなりますが、テキストをタップしているような感覚が得られます。
CGPathRef endPath = CGPathCreateMutableCopyByTransformingPath(letters, &t);
CGMutablePathRef finalPath = CGPathCreateMutableCopy(endPath);
CGPathRef strokedPath = CGPathCreateCopyByStrokingPath(endPath, NULL, 7, kCGLineCapRound, kCGLineJoinRound, 0);
CGPathAddPath(finalPath, NULL, strokedPath);
// Clean up all the unused paths
CGPathRelease(strokedPath);
CGPathRelease(letters);
CGPathRelease(endPath);
self.textPath = finalPath;
下の画像のオレンジ色の領域もタップ可能になります。これでもテキストに触れているように感じられますが、アプリのユーザーの煩わしさは軽減されます。
必要に応じて、これをさらに進めてテキストをヒットしやすくすることもできますが、ある時点で、ラベル全体がタップ可能になっているように感じるでしょう。