iOSでのテキスト出力:CoreText、NSAttributedString

iOSの画面にテキストを表示することについてお話ししたいと思います。 最初はチュートリアルとして、次に非自明な事実のコレクションとして考案されましたが、何が起こったのかが判明しました。



テキストを表示する方法は、条件付きでいくつかのカテゴリに分類できます。







NSAttributedString



最初に、フリーダイヤルブリッジオブジェクトを使用する場合は、 NSAttributedStringまたはCFAttributedStringRefと呼ばれる基本的なことについて話す必要があります。 これは、表示属性を設定できる文字列です。



NSDictionary *textAttributes = @{(NSString *)kCTFontAttributeName : [UIFont systemFontOfSize:16]}; NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:LoremIpsum attributes:textAttributes];
      
      





このコードは、単一の表示属性を持つ線を作成します。線を描画するときに使用するフォントです。 次のようになります。





ここで、DemoniacDeathがコメントで親切に思い出させてくれた1つの重要なポイントを強調する必要があります。CoreTextは、レンダリングにCTFontを使用します。 したがって、簡潔さの例で使用されている型キャストはiOS6でのみ機能し、以下のすべてについて、UIFontに基づいてCTFontを作成する必要があります。



 CTFont ctFont = (__bridge_transfer id)CTFontCreateWithName(uiFont.fontName, uiFont.pointSize, NULL);
      
      





そのため、使用可能な標準属性がかなりあります。





たとえば、カーニング3.0、フォントサイズ14、下線付きのテキストは、右側のすべてのテキストを20.0ポイント、赤線を40.0ポイント、30.0ポイントの段落間隔でインデントします。



 CTParagraphStyleSetting paragraphSettings[] = (CTParagraphStyleSetting[]){ (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(float_t), (float_t[]){ 40.0f } }, (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierHeadIndent, sizeof(float_t), (float_t[]){ 20.0f } }, (CTParagraphStyleSetting){ kCTParagraphStyleSpecifierParagraphSpacing, sizeof(float_t), (float_t[]){ 30.0f } } }; id paragraphStyle = (__bridge_transfer id)CTParagraphStyleCreate(paragraphSettings, sizeof(paragraphSettings) / sizeof(paragraphSettings[0])); NSDictionary *textAttributes = @{ (NSString *)kCTFontAttributeName : [UIFont systemFontOfSize:14], (NSString *)kCTKernAttributeName : @(3.0), (NSString *)kCTUnderlineStyleAttributeName : @(kCTUnderlineStyleSingle), (NSString *)kCTParagraphStyleAttributeName : paragraphStyle };
      
      









描画



ウイキット



次に、これらすべてをレンダリングする方法について説明します。 iOS6以降、 NSAttributedStringにはUIKitにNSStringDrawingカテゴリがあり 、これを簡単に行うことができます。



 NSDictionary *textAttributes = @{(NSString *)kCTFontAttributeName : [UIFont systemFontOfSize:16]}; NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:LoremIpsum attributes:textAttributes]; [attributedString drawInRect:rect];
      
      





別の方法があります:



 - (void)drawWithRect:(CGRect)rect options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context;
      
      





これは、渡されたパラメーターと特別なコンテキストを使用してカスタマイズ可能になります。 率直に言って、私たちが製品でサポートするiOSの最小バージョンは4.3であるため、これらのメソッドを使用したことはありません。

作成されたNSAttributedStringsetAttributedString:メソッドを介してUILabelUITextField、またはUITextViewに渡すこともできます。 このメソッドもiOS6.0でのみサポートされています。 ちなみに、iOS6.0以降のこれらの要素では、表示の属性をxibまたはストーリーボードのInterfaceBuilderで直接設定できます。



コアテキスト



CoreTextは、抽象化のいくつかのレベルでテキストを描画できるようにする素晴らしいものです。 また、いくつかのレベルでテキスト表示の基本部分にアクセスできます。

さらに説明を始める前に、この瞬間までに、グラフィックコンテキストとは何かを知っており、アフィン変換に精通していることを理解したいと思います。

また、Apple Webサイトから写真を盗みます。







最初のレベル。 CTFramesetter


このレベルでは、まだ描画できません。 この抽象化の唯一の役割は、指定されたスタイルのテキストを使用して、幾何プリミティブに内接する描画オブジェクトを作成することです。 また、 CTFramesetterを使用すると、幅または高さを修正して、現在の表示設定で現在のテキストの高さまたは幅を見つけることができます。



 NSDictionary *textAttributes = @{(NSString *)kCTFontAttributeName : [UIFont systemFontOfSize:16]}; NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:LoremIpsum attributes:textAttributes]; CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(attributedString)); CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(CGRectGetWidth(rect), CGFLOAT_MAX), NULL);
      
      





ここでは、テキストを入力する幅が固定されています。 suggestSizeには、目的のテキストの高が含まれます。 同じことは幅で行うことができます。



第2レベル。 CTFrame


このオブジェクトは、上記のCTFramesetterを作成し、すでに完全にレンダリングの準備ができています。



 CGPathRef path = CGPathCreateWithRect((CGRect){CGPointZero, suggestedSize}, NULL); CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL); CTFrameDraw(textFrame, context);
      
      





次のメソッドのパラメーターについて詳しく説明します。



 CTFrameRef CTFramesetterCreateFrame( CTFramesetterRef framesetter, CFRange stringRange, CGPathRef path, CFDictionaryRef frameAttributes )
      
      







いくつかの例。



 CGMutablePathRef path = CGPathCreateMutable(); CGPathAddEllipseInRect(path, NULL, (CGRect){CGPointZero, CGSizeMake(suggestedSize.width, suggestedSize.height / 2)}); CGPathAddEllipseInRect(path, NULL, (CGRect){0, suggestedSize.height / 2, CGSizeMake(suggestedSize.width, suggestedSize.height / 2)});
      
      









 CGAffineTransform transformation = CGAffineTransformMakeRotation(M_PI_4); CGPathAddEllipseInRect(path, &transformation, CGRectMake(20, -200, 400, 130)); CGPathAddRect(path, &transformation, CGRectMake(50, -150, 200, 200));
      
      









最後の例では、 CTFrameにkCTFramePathFillWindingNumberパラメーターも使用しているため、交差する領域にもテキストが入力されます。



他の2つの重要なポイント:

  1. 上記の例のいずれかを試してみると、結果は私から与えられた結果とは非常に異なります。つまり、画像は反転します。CoreTextは、たとえばCGBitmapContextのように作成されたCGContextのように左下隅から始まる画像を形成するためです したがって、正しい出力を得るには、現在のグラフィックスコンテキストを変換する必要があります
  2. グラフィックコンテキスト全体ではなく、テキストのみに変換を指定することが可能であり、必要です。 これはCGContextSetTextMatrixを介して行われます。 このことの特徴は、 CGSave / RestoreContextがテキストの変換マトリックスを保存または復元しないことです。 したがって、レンダリングの前に常に設定する必要があります。そうしないと、システムライブラリの腸またはプログラムの他の部分でこのメソッドを呼び出すときに設定された値が含まれる場合があります。


 CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextScaleCTM(context, 1.0f, -1.0f); CGContextTranslateCTM(context, 0.0f, -suggestedSize.height);
      
      





非恒等変換行列はどうなりますか:



 CGContextSetTextMatrix(context, CGAffineTransformMakeRotation(M_PI_4)); CGContextScaleCTM(context, 1.0f, -1.0f); CGContextTranslateCTM(context, 0.0f, -suggestedSize.height);
      
      









さらに議論する前に、AppleのWebサイトから別の写真を盗みたいと思います。







叙情的な余談。 CTTypesetter


CTFrameの作成に関与するのはこのエンティティです。 その役割は、この段階ですでにレンダリングの準備ができている非常にテキスト形式のグラフィックプリミティブを作成することです。 与えられたアルゴリズム(単語、文字など)に従って改行を実行し、適合しなかったものをすべて切り取ります。



レベル3。 CTLine


これがCTFrameの構成要素です。 CTLineは、配置する位置を設定することで描画できます。CTFrameGetLineOriginsメソッドを使用してCTFrameから計算するか、非常にトリッキーなアルゴリズムに従って設定します。



 CFArrayRef lines = CTFrameGetLines(textFrame); for (CFIndex i = 0, linesCount = CFArrayGetCount(lines); i < linesCount; ++i) { CGPoint lineOrigin = CGPointZero; CTFrameGetLineOrigins(textFrame, CFRangeMake(i, 1), &lineOrigin); CGContextSetTextPosition(context, lineOrigin.x, lineOrigin.y); CTLineDraw(CFArrayGetValueAtIndex(lines, i), context); }
      
      





他に知っておく価値のあるものは間違いありません。 この行には、 ベースラインアセントディセントリーディングといういくつかの印刷上のパラメーターがあります。 ここでみんなについて読むことができますCTFrameGetLineOriginsはちょうどbaselineを返します。 残りのパラメーターは、メソッドを通じて見つけることができます。



 doubCTLineGetTypographicBounds(CTLineRef line, CGFloat* ascent, CGFloat* descent, CGFloat* leading);
      
      







赤は下降、黒はベースライン、青は上昇です。



これらのパラメーターに戻ります。 それまでの間、ラインで使用されるすべてのフォントの最小値と最大値として、ライン全体で下降上昇が行われることに注意してください。 たとえば、上記のテキストの場合、ヘブライ語が存在する行では、 降下が大きいことがはっきりとわかります。



より便利な方法:



 CFRange CTLineGetStringRange(CTLineRef line)
      
      





現在の行の文字列間隔を返します。 選択した文字列間隔に該当する特定の行のみが必要な場合に、アルゴリズムでよく使用されます。



 CFIndex CTLineGetStringIndexForPosition(CTLineRef line, CGPoint position)
      
      





文字列内の指定された位置の文字列インデックスを返します。 たとえば、テキストの特定の領域のクリックを判別するために使用できます。 コンテキストが上下逆になっており、(0,0)が最終行にあることに注意してください。



 CTLineRef CTLineCreateWithAttributedString(CFAttributedStringRef string)
      
      





線を引くために、CTFramesetter-> CTFrame-> CTLineからチェーン全体を作成する必要はありません。 すぐに行を作成できます。 多くの場合、テキストが特定の領域に収まらない場合は、タスクをトリミングすることもあります。



 CTLineRef CTLineCreateTruncatedLine(CTLineRef line, double width, CTLineTruncationType truncationType, CTLineRef truncationToken)
      
      





第4レベル。 CTrun


おそらく、テキストを入力するための独自の要素を作成する人にとって最も便利な抽象化です。 CTRunは、同じスタイルの文字をグループ化してグループ化する場所です。 むしろ、それらはiOS6.0からグループ化されており、それより若いものではすべてのキャラクターが個別のCTRunになります。 最初から、簡単で明白な1つのポイントを明確にする必要があります 。シンボルがありますが、 グリフがあります 。 この事実から、他にも2つのポイントが続きますが、これも簡単ですが、すでに明らかではありません。 1つのグリフで一度に複数の文字を表すことができます。 たとえば、ほとんどの絵文字は単一のグリフですが、いくつかのUTF文字で構成されています。

CTRunを使用するすべての人には、 CTRun.hに慣れることを強くお勧めします-そこにあるすべてのメソッドは非常に便利です。



 CFIndex CTRunGetGlyphCount(CTRunRef run)
      
      





CTRunのグリフの数を返します。



 CFRange CTRunGetStringRange(CTRunRef run)
      
      





CTRunが配置されている文字列全体から範囲を返します。 この範囲の長さは、常に前の方法の結果と同じではありません!



 const CFIndex* CTRunGetStringIndicesPtr(CTRunRef run) void CTRunGetStringIndices(CTRunRef run, CFRange range, CFIndex buffer[])
      
      





このメソッドは前のメソッドに関連しています。 文字列内のグリフの位置を返します。 たとえば、いくつかの絵文字のテキストがあります。





iOS6.0以降、 CTrunは次のようになります。

CTRun: string range = (0, 6), string = "\U0001f437\U0001f434\U0001f428"





ご覧のとおり、各グリフは実際には2つの文字で構成されています。 CTRunGetStringIndicesメソッドを呼び出した結果は[0,2,4]になります。



このメソッドには2つのオプションがあることに注意してください。必要な数のインデックスを事前に割り当てられたバッファーにコピーするか、 CTRun内の配列の先頭へのポインターを取得します。 より便利なものを選択してください。 そのような方法はたくさんありますが、これは署名から見ることができます。私はこれに焦点を合わせません。



 CFDictionaryRef CTRunGetAttributes(CTRunRef run)
      
      





CFAttributedStringで設定されたCTRunの現在の属性。



 CTRunStatus CTRunGetStatus(CTRunRef run)
      
      





ビットマスクの形式で、ディスプレイ固有のパラメーターを返します。 kCTRunStatusRightToLeftに特に注意してください-CTRunのグリフには右から左への綴り方向があることを示しています。



 const CGPoint* CTRunGetPositionsPtr(CTRunRef run) void CTRunGetPositions(CTRunRef run, CFRange range, CGPoint buffer[])
      
      





CTFrameに対するCTRun内のグリフの位置。 メソッドの2番目のバージョンを使用して、たとえば2番目のグリフの位置を取得する場合、範囲内で1(番号付けは0から始まる)ではなく、行内のこのグリフの位置を渡す必要があるという事実に特に注意を払います。 上記の絵文字を持つバリアントの場合、これは位置2になります。これは、すべての同様の方法に適用されます。



 const CGSize* CTRunGetAdvancesPtr(CTRunRef run) void CTRunGetAdvances(CTRunRef run, CFRange range, CGSize buffer[])
      
      





グリフのサイズを返します。



 double CTRunGetTypographicBounds(CTRunRef run, CFRange range, CGFloat* ascent, CGFloat* descent, CGFloat* leading)
      
      





単一のCTRunの活版印刷パラメーターを返します。 覚えておいて、このラインで使用されたフォントでは、ラインの下降上昇は極端なものと見なされると言ったのですか? したがって、行の各部分について、個別に認識できるようになりました。





また、 下降上昇が明確になったら、 kCTRunDelegateAttributeName属性について説明します。 入力では、いくつかのメソッドに基づいて作成されるCTRunDelegateを使用します。

 typedef struct { CFIndex version; CTRunDelegateDeallocateCallback dealloc; CTRunDelegateGetAscentCallback getAscent; CTRunDelegateGetDescentCallback getDescent; CTRunDelegateGetWidthCallback getWidth; } CTRunDelegateCallbacks;
      
      





最後の3つに興味があります。 メソッドは、この属性でタグ付けされたグリフのディセントアセント、幅を返す無料の関数です。 現在のグリフのパラメーターが一般的な幾何学的パラメーターの計算に参加するたびに、一連のメソッドが呼び出されます。 これを使用して、独自のグリフを作成できます。



 void CTRunDraw(CTRunRef run, CGContextRef context, CFRange range)
      
      





グラフィックコンテキストでCTRunを描画します。



Coregraphics



第5レベル。 グリフ


率直に言って、このレベルはもはやCoreTextではなく、 CoreGraphicsです。以前は利用可能でした。 グリフを取得するのが簡単になったのは今です。



 CFIndex glyphCount = CTRunGetGlyphCount(run); CGPoint positions[glyphCount]; CGGlyph glyphs[glyphCount]; CTRunGetPositions(run, CFRangeMake(0, 0), positions); CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); CGContextSetFont(context, cgFont); CGContextSetFontSize(context, CTFontGetSize(runFont)); CGContextSetFillColorWithColor(context, runColor); CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
      
      





この場合、下線スタイルはCoreTextを介して実装されるため、使用できないことを理解する必要があります。 ただし、レンダリング時には、自由にグリフを配置できます。



Appleはいつものように、API設計の伝統を忠実に守っています。単純なことは遠くまで行けず、単純に「do Me Good」メソッドを呼び出すことはできません。



All Articles