与えられたNSRange
について、そのCGRect
のグリフに対応するUILabel
でNSRange
を見つけたいです。たとえば、「速い茶色のキツネが怠け者の犬を飛び越えます」という文に「dog」という単語を含むCGRect
を見つけたいです。
トリックは、UILabel
に複数の行があり、テキストが実際にattributedText
であるため、文字列の正確な位置を見つけるのは少し難しいことです。
UILabel
サブクラスに書き込みたいメソッドは次のようになります。
- (CGRect)rectForSubstringWithRange:(NSRange)range;
興味のある人のための詳細:
これに関する私の目標は、UILabelの正確な外観と位置を使用して新しいUILabelを作成し、アニメーション化できるようにすることです。しかし、現時点で私を阻んでいるのは、特にこのステップです。
これまでに問題を解決しようと試みたもの:
UITextView
とUITextField
に焦点を当てていますUILabel
ではなく。これに対する正しい答えは、次のいずれかを含むことは間違いないでしょう:
NSLayoutManager
とtextContainerForGlyphAtIndex:effectiveRange
が関係するに違いない更新:これは、この問題を解決するためにこれまで試してきた3つのことを示すgithubの要旨です。 https://Gist.github.com/bryanjclark/7036101
コードで ジョシュアの答え に続いて、うまくいくように見える次のものを思いつきました:
- (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];
}
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)
私の提案は、テキストキットを使用することです。残念ながら、UILabel
が使用するレイアウトマネージャーにアクセスすることはできませんが、そのレプリカを作成し、それを使用して範囲の四角形を取得することは可能です。
私の提案は、ラベルとまったく同じ属性付きテキストを含むNSTextStorage
オブジェクトを作成することです。次にNSLayoutManager
を作成し、それをテキストストレージオブジェクトに追加します。最後に、ラベルと同じサイズのNSTextContainer
を作成し、レイアウトマネージャーに追加します。
これで、テキストストレージはラベルと同じテキストを持ち、テキストコンテナはラベルと同じサイズになるので、boundingRectForGlyphRange:inTextContainer:
を使用して範囲の四角形を作成したレイアウトマネージャーに問い合わせることができます。レイアウトマネージャオブジェクトでglyphRangeForCharacterRange:actualCharacterRange:
を使用して、まず文字範囲をグリフ範囲に変換してください。
すべてうまくいけば、ラベル内で指定した範囲のboundingCGRect
が得られます。
私はこれをテストしていませんが、これは私のアプローチであり、UILabel
自体がどのように機能するかを模倣することで成功する可能性が十分にあります。
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)
}
}
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)
}
適切な答えは、実際にはできないということです。ここでのソリューションのほとんどは、さまざまな精度でUILabel
内部動作を再作成しようとしています。最近のiOSバージョンでは、UILabel
がTextKit/CoreTextフレームワークを使用してテキストをレンダリングします。以前のバージョンでは、実際にWebKitを内部で使用していました。将来的にそれが何を使用するかを誰が知っているか。 TextKitでの複製には、現在でも明らかな問題があります(将来性のあるソリューションについては説明していません)。テキストのレイアウトとレンダリングが実際にどのように実行されるか、つまりどのパラメーターが使用されるかはよくわかりません(または保証できません)。言及されたすべてのEdgeケースを見てください。いくつかのソリューションは、テストしたいくつかの簡単なケースで「偶然」機能する場合があります。これは、iOSの現在のバージョン内でも何も保証しません。テキストのレンダリングは難しいので、RTLサポート、Dynamic Type、絵文字などを始めないでください。
適切なソリューションが必要な場合は、UILabel
を使用しないでください。これは単純なコンポーネントであり、そのTextKit内部がAPIで公開されていないという事実は、Appleこの実装の詳細に依存するソリューションを開発者に構築させたくないことを明確に示しています。
または、UITextViewを使用できます。 TextKitの内部を便利に公開し、非常に構成可能なので、UILabel
が役立つほとんどすべてを達成できます。
また、下位レベルに移動して、TextKitを直接使用することもできます(またはCoreTextでさらに低くすることもできます)。実際にテキスト位置を見つける必要がある場合、これらのフレームワークで利用可能な他の強力なツールがすぐに必要になる可能性があります。最初は使いづらいですが、複雑さのほとんどは、テキストのレイアウトとレンダリングの仕組みを実際に学ぶことから来ているように感じます。 Appleの優れたテキストフレームワークに慣れると、テキストでこれ以上のことができるようになります。
代わりに、UITextViewに基づいてクラスを作成できますか?その場合は、UiTextInputプロトコルメソッドを確認してください。特にジオメトリを参照し、休息方法をヒットします。
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 の回答を更新しました。