Xamarin.iOS の TextKit

TextKit は、強力なテキスト レイアウトとレンダリングの機能を備える新しい API です。 これは、低レベルの Core Text フレームワーク上に構築されていますが、Core Text よりもはるかに簡単に使用できます。

TextKit の機能を標準コントロールで使用できるようにするために、次のようないくつかの iOS テキスト コントロールで TextKit を使用するように再実装されています。

  • UITextView
  • UITextField
  • UILabel

Architecture

TextKit には、次のクラスを含む、レイアウトと表示からテキスト ストレージを分離する階層化アーキテクチャが用意されています。

  • NSTextContainer – テキストのレイアウトに使用される座標系とジオメトリを提供します。
  • NSLayoutManager – テキストをグリフに変換してテキストをレイアウトします。
  • NSTextStorage – テキスト データを保持し、バッチ テキスト プロパティの更新を処理します。 バッチ更新は、レイアウトの再計算やテキストの再描画など、変更の実際の処理のためにレイアウト マネージャーに渡されます。

これら 3 つのクラスは、テキストをレンダリングするビューに適用されます。 組み込みのテキスト処理ビュー (UITextViewUITextFieldUILabel が既に設定済みの場合など) ですが、これらを作成して任意の UIView インスタンスに適用することもできます。

このアーキテクチャを次の図に示します。

この図は、TextKit アーキテクチャを示します

テキスト ストレージと属性

NSTextStorage クラスは、ビューによって表示されるテキストを保持します。 また、テキストへの変更 (文字や属性の変更など) もレイアウト マネージャーに通知して表示します。 NSTextStorageMSMutableAttributed 文字列から継承され、テキスト属性に対する変更を BeginEditingEndEditing の間のバッチに指定できます。

たとえば、次のコード スニペットでは、前景色と背景色の変更をそれぞれ指定し、特定の範囲を対象にしています。

textView.TextStorage.BeginEditing ();
textView.TextStorage.AddAttribute(UIStringAttributeKey.ForegroundColor, UIColor.Green, new NSRange(200, 400));
textView.TextStorage.AddAttribute(UIStringAttributeKey.BackgroundColor, UIColor.Black, new NSRange(210, 300));
textView.TextStorage.EndEditing ();

EndEditing が呼び出されると、変更がレイアウト マネージャーに送信され、ビューに表示されるテキストに必要なレイアウトとレンダリングの計算が実行されます。

除外パスを含むレイアウト

TextKit はレイアウトもサポートしており、複数列のテキストや、除外パスと呼ばれる指定パスの周囲にテキストを流すといった複雑なシナリオも可能です。 除外パスはテキスト コンテナーに適用され、テキスト レイアウトのジオメトリが変更され、指定されたパスの周囲にテキストが流れます。

除外パスを追加するには、レイアウト マネージャーで ExclusionPaths プロパティを設定する必要があります。 このプロパティを設定すると、レイアウト マネージャーはテキスト レイアウトを無効にし、除外パスの周囲にテキストを流します。

CGPath に基づく除外

次の UITextView サブクラスの実装を見てみましょう。

public class ExclusionPathView : UITextView
{
    CGPath exclusionPath;
    CGPoint initialPoint;
    CGPoint latestPoint;
    UIBezierPath bezierPath;

    public ExclusionPathView (string text)
    {
        Text = text;
        ContentInset = new UIEdgeInsets (20, 0, 0, 0);
        BackgroundColor = UIColor.White;
        exclusionPath = new CGPath ();
        bezierPath = UIBezierPath.Create ();

        LayoutManager.AllowsNonContiguousLayout = false;
    }

    public override void TouchesBegan (NSSet touches, UIEvent evt)
    {
        base.TouchesBegan (touches, evt);

        var touch = touches.AnyObject as UITouch;

        if (touch != null) {
            initialPoint = touch.LocationInView (this);
        }
    }

    public override void TouchesMoved (NSSet touches, UIEvent evt)
    {
        base.TouchesMoved (touches, evt);

        UITouch touch = touches.AnyObject as UITouch;

        if (touch != null) {
            latestPoint = touch.LocationInView (this);
            SetNeedsDisplay ();
        }
    }

    public override void TouchesEnded (NSSet touches, UIEvent evt)
    {
        base.TouchesEnded (touches, evt);

        bezierPath.CGPath = exclusionPath;
        TextContainer.ExclusionPaths = new UIBezierPath[] { bezierPath };
    }

    public override void Draw (CGRect rect)
    {
        base.Draw (rect);

        if (!initialPoint.IsEmpty) {

            using (var g = UIGraphics.GetCurrentContext ()) {

                g.SetLineWidth (4);
                UIColor.Blue.SetStroke ();

                if (exclusionPath.IsEmpty) {
                    exclusionPath.AddLines (new CGPoint[] { initialPoint, latestPoint });
                } else {
                    exclusionPath.AddLineToPoint (latestPoint);
                }

                g.AddPath (exclusionPath);
                g.DrawPath (CGPathDrawingMode.Stroke);
            }
        }
    }
}

このコードでは、コア グラフィックスを使用したテキスト ビューでの描画のサポートが追加されています。 テキストのレンダリングとレイアウトに TextKit を使用するように UITextView クラスが構築されたので、除外パスの設定など、TextKit のすべての機能を使用できます。

重要

この例では、タッチ描画のサポートを追加するために UITextView をサブクラス化します。 TextKit の機能を取得するために UITextView をサブクラス化する必要はありません。

ユーザーがテキスト ビューに描画した後、描画された CGPathUIBezierPath.CGPath プロパティを設定して UIBezierPath インスタンスに適用されます。

bezierPath.CGPath = exclusionPath;

次のコード行を更新すると、パスの周囲のテキスト レイアウトが更新されます。

TextContainer.ExclusionPaths = new UIBezierPath[] { bezierPath };

次のスクリーンショットは、テキスト レイアウトが描画されたパスの周りを流れるように変化する方法を示しています。

このスクリーンショットは、テキスト レイアウトが描画されたパスの周りを流れるように変化する方法を示します

この場合、レイアウト マネージャーの AllowsNonContiguousLayout プロパティが false に設定されていることに注意してください。 これにより、テキストが変更されるすべてのケースでレイアウトが再計算されます。 これを true に設定すると、特に大きなドキュメントの場合は、全体のレイアウトを更新しないようにすることでパフォーマンスが向上する可能性があります。 ただし、AllowsNonContiguousLayout を true に設定すると、一部の状況で除外パスがレイアウトを更新できなくなります。たとえば、パスが設定される前に末尾のキャリッジ リターンなしで実行時にテキストが入力された場合などです。