web-dev-qa-db-ja.com

UILabelのテキストのサブストリングのCGRectを見つけるにはどうすればよいですか?

与えられたNSRangeについて、そのCGRectのグリフに対応するUILabelNSRangeを見つけたいです。たとえば、「速い茶色のキツネが怠け者の犬を飛び越えます」という文に「dog」という単語を含むCGRectを見つけたいです。

Visual description of problem

トリックは、UILabelに複数の行があり、テキストが実際にattributedTextであるため、文字列の正確な位置を見つけるのは少し難しいことです。

UILabelサブクラスに書き込みたいメソッドは次のようになります。

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

興味のある人のための詳細:

これに関する私の目標は、UILabelの正確な外観と位置を使用して新しいUILabelを作成し、アニメーション化できるようにすることです。しかし、現時点で私を阻んでいるのは、特にこのステップです。

これまでに問題を解決しようと試みたもの:

  • IOS 7では、この問題を解決するText Kitが少しあればいいのにと思いましたが、Text Kitで見たほとんどの例はすべてUITextViewUITextFieldに焦点を当てていますUILabelではなく。
  • ここでスタックオーバーフローに関する別の質問を見ましたが、これは問題の解決を約束しますが、受け入れられた答えは2年以上前であり、コードは属性付きテキストではうまく機能しません。

これに対する正しい答えは、次のいずれかを含むことは間違いないでしょう:

  • 標準のテキストキットメソッドを使用して、1行のコードでこの問題を解決します。 NSLayoutManagertextContainerForGlyphAtIndex:effectiveRangeが関係するに違いない
  • UILabelを行に分割し、おそらくコアテキストメソッドを使用して、行内のグリフの四角形を見つける複雑なメソッドを記述します。私の現在の最善策は、分解することです @ mattt's すばらしい TTTAttributedLabel グリフ、それはうまくいくかもしれません。

更新:これは、この問題を解決するためにこれまで試してきた3つのことを示すgithubの要旨です。 https://Gist.github.com/bryanjclark/7036101

69
bryanjclark

コードで ジョシュアの答え に続いて、うまくいくように見える次のものを思いつきました:

- (CGRect)boundingRectForCharacterRange:(NSRange)range
{
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [textStorage addLayoutManager:layoutManager];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
    textContainer.lineFragmentPadding = 0;
    [layoutManager addTextContainer:textContainer];

    NSRange glyphRange;

    // Convert the range for glyphs.
    [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];

    return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
}
88
Luke Rogers

Luke Rogers's answer で構築されていますが、Swiftで記述されています:

スイフト2

extension UILabel {
    func boundingRectForCharacterRange(_ range: NSRange) -> CGRect? {

        guard let attributedText = attributedText else { return nil }

        let textStorage = NSTextStorage(attributedString: attributedText)
        let layoutManager = NSLayoutManager()

        textStorage.addLayoutManager(layoutManager)

        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0.0

        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer: textContainer)        
    }
}

使用例(Swift 2)

let label = UILabel()
let text = "aa bb cc"
label.attributedText = NSAttributedString(string: text)
let sublayer = CALayer()
sublayer.borderWidth = 1
sublayer.frame = label.boundingRectForCharacterRange(NSRange(text.range(of: "bb")!, in: text))
label.layer.addSublayer(sublayer)

Swift 3/4

extension UILabel {
    func boundingRect(forCharacterRange range: NSRange) -> CGRect? {

        guard let attributedText = attributedText else { return nil }

        let textStorage = NSTextStorage(attributedString: attributedText)
        let layoutManager = NSLayoutManager()

        textStorage.addLayoutManager(layoutManager)

        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0.0

        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)       
    }
}

使用例(Swift 3/4)

let label = UILabel()
let text = "aa bb cc"
label.attributedText = NSAttributedString(string: text)
let sublayer = CALayer()
sublayer.borderWidth = 1
sublayer.frame = label.boundingRect(forCharacterRange: NSRange(text.range(of: "bb")!, in: text))
label.layer.addSublayer(sublayer)
16
NoodleOfDeath

私の提案は、テキストキットを使用することです。残念ながら、UILabelが使用するレイアウトマネージャーにアクセスすることはできませんが、そのレプリカを作成し、それを使用して範囲の四角形を取得することは可能です。

私の提案は、ラベルとまったく同じ属性付きテキストを含むNSTextStorageオブジェクトを作成することです。次にNSLayoutManagerを作成し、それをテキストストレージオブジェクトに追加します。最後に、ラベルと同じサイズのNSTextContainerを作成し、レイアウトマネージャーに追加します。

これで、テキストストレージはラベルと同じテキストを持ち、テキストコンテナはラベルと同じサイズになるので、boundingRectForGlyphRange:inTextContainer:を使用して範囲の四角形を作成したレイアウトマネージャーに問い合わせることができます。レイアウトマネージャオブジェクトでglyphRangeForCharacterRange:actualCharacterRange:を使用して、まず文字範囲をグリフ範囲に変換してください。

すべてうまくいけば、ラベル内で指定した範囲のboundingCGRectが得られます。

私はこれをテストしていませんが、これは私のアプローチであり、UILabel自体がどのように機能するかを模倣することで成功する可能性が十分にあります。

11
Joshua

Swift 4ソリューションは、複数行の文字列でも動作し、境界はintrinsicContentSizeに置き換えられました

extension UILabel {
func boundingRectForCharacterRange(range: NSRange) -> CGRect? {

    guard let attributedText = attributedText else { return nil }

    let textStorage = NSTextStorage(attributedString: attributedText)
    let layoutManager = NSLayoutManager()

    textStorage.addLayoutManager(layoutManager)

    let textContainer = NSTextContainer(size: intrinsicContentSize)

    textContainer.lineFragmentPadding = 0.0

    layoutManager.addTextContainer(textContainer)

    var glyphRange = NSRange()

    layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

    return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
}
4
Dima

Swift 3。

func boundingRectForCharacterRange(_ range: NSRange) -> CGRect {
        let textStorage = NSTextStorage(attributedString: self.attributedText!)
        let layoutManager = NSLayoutManager()
        textStorage.addLayoutManager(layoutManager)
        let textContainer = NSTextContainer(size: self.bounds.size)
        textContainer.lineFragmentPadding = 0
        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    }
2
Nico Achkasov

適切な答えは、実際にはできないということです。ここでのソリューションのほとんどは、さまざまな精度でUILabel内部動作を再作成しようとしています。最近のiOSバージョンでは、UILabelがTextKit/CoreTextフレームワークを使用してテキストをレンダリングします。以前のバージョンでは、実際にWebKitを内部で使用していました。将来的にそれが何を使用するかを誰が知っているか。 TextKitでの複製には、現在でも明らかな問題があります(将来性のあるソリューションについては説明していません)。テキストのレイアウトとレンダリングが実際にどのように実行されるか、つまりどのパラメーターが使用されるかはよくわかりません(または保証できません)。言及されたすべてのEdgeケースを見てください。いくつかのソリューションは、テストしたいくつかの簡単なケースで「偶然」機能する場合があります。これは、iOSの現在のバージョン内でも何も保証しません。テキストのレンダリングは難しいので、RTLサポート、Dynamic Type、絵文字などを始めないでください。

適切なソリューションが必要な場合は、UILabelを使用しないでください。これは単純なコンポーネントであり、そのTextKit内部がAPIで公開されていないという事実は、Appleこの実装の詳細に依存するソリューションを開発者に構築させたくないことを明確に示しています。

または、UITextViewを使用できます。 TextKitの内部を便利に公開し、非常に構成可能なので、UILabelが役立つほとんどすべてを達成できます。

また、下位レベルに移動して、TextKitを直接使用することもできます(またはCoreTextでさらに低くすることもできます)。実際にテキスト位置を見つける必要がある場合、これらのフレームワークで利用可能な他の強力なツールがすぐに必要になる可能性があります。最初は使いづらいですが、複雑さのほとんどは、テキストのレイアウトとレンダリングの仕組みを実際に学ぶことから来ているように感じます。 Appleの優れたテキストフレームワークに慣れると、テキストでこれ以上のことができるようになります。

1
Max O

代わりに、UITextViewに基づいてクラスを作成できますか?その場合は、UiTextInputプロトコルメソッドを確認してください。特にジオメトリを参照し、休息方法をヒットします。

1
pickwick

plain text拡張機能をお探しの方に!

extension UILabel {    
    func boundingRectForCharacterRange(range: NSRange) -> CGRect? {
        guard let text = text else { return nil }

        let textStorage = NSTextStorage.init(string: text)
        let layoutManager = NSLayoutManager()

        textStorage.addLayoutManager(layoutManager)

        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0.0

        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    }
}

追伸Noodle of Death`sanswer の回答を更新しました。

0
Hemang