Pour une NSRange
donnée, j'aimerais trouver une CGRect
dans une UILabel
qui correspond aux glyphes de cette NSRange
. Par exemple, j'aimerais rechercher la variable CGRect
contenant le mot "chien" dans la phrase "Le renard brun rapide saute par-dessus le chien paresseux".
Le truc, c'est que la UILabel
a plusieurs lignes et que le texte est vraiment attributedText
, il est donc un peu difficile de trouver la position exacte de la chaîne.
La méthode que j'aimerais écrire dans ma sous-classe UILabel
ressemblerait à ceci:
- (CGRect)rectForSubstringWithRange:(NSRange)range;
_ {Détails, pour ceux qui sont intéressés:} _
Mon objectif est de pouvoir {créer un nouveau UILabel avec l’apparence et la position exactes du UILabel, que je pourrai ensuite animer.} J’ai compris le reste, mais c’est cette étape en particulier cela me retient pour le moment.
Ce que j'ai fait pour essayer de résoudre le problème jusqu'à présent:
UITextView
et UITextField
, plutôt que UILabel
.Je parierais que la bonne réponse à cette question implique l'un des éléments suivants:
NSLayoutManager
et textContainerForGlyphAtIndex:effectiveRange
Mise à jour: Voici un github Gist avec les trois solutions que j'ai déjà essayées pour résoudre ce problème: https://Gist.github.com/bryanjclark/7036101
Suite à la réponse de Joshua dans le code, je propose ce qui suit qui semble bien fonctionner:
- (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];
}
Construire à partir de la réponse de Luke Rogers mais écrite en Swift:
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)
}
}
Exemple d'utilisation (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)
}
}
Exemple d'utilisation (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)
Ma suggestion serait d'utiliser le kit de texte. Malheureusement, nous n'avons pas accès au gestionnaire de disposition utilisé par une variable UILabel
. Toutefois, il est possible de créer une réplique de celle-ci et de l'utiliser pour obtenir le droit d'une plage.
Ma suggestion serait de créer un objet NSTextStorage
contenant exactement le même texte attribué que dans votre étiquette. Créez ensuite une NSLayoutManager
et ajoutez-la à l'objet de stockage de texte. Enfin, créez une NSTextContainer
de la même taille que l’étiquette et ajoutez-la au gestionnaire de disposition.
Le stockage de texte a maintenant le même texte que l'étiquette et le conteneur de texte a la même taille que l'étiquette. Nous devrions donc pouvoir demander au gestionnaire de disposition que nous avons créé pour un rect de notre plage en utilisant boundingRectForGlyphRange:inTextContainer:
. Assurez-vous de convertir d'abord votre plage de caractères en plage de glyphes en utilisant glyphRangeForCharacterRange:actualCharacterRange:
sur l'objet du gestionnaire de disposition.
Tout va bien qui devrait vous donner une bounding CGRect
de la plage que vous avez spécifiée dans l’étiquette.
Je n'ai pas testé cela, mais ce serait mon approche et en imitant le fonctionnement de UILabel
, elle-même devrait avoir de bonnes chances de réussir.
Swift 4 solution, fonctionnera même pour les chaînes multilignes, les limites ont été remplacées par 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)
}
}
Traduit pour 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)
}
Pouvez-vous plutôt baser votre classe sur UITextView? Si tel est le cas, consultez les méthodes du protocole UiTextInput. Voir en particulier la géométrie et les méthodes de repos.
Pour tous ceux qui recherchent une extension 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)
}
}
P.S. Mise à jour de la réponse de Noodle of Death`sanswer .
La bonne réponse est que vous ne pouvez pas réellement. La plupart des solutions tentent ici de recréer le comportement interne de UILabel
avec une précision variable. Il se trouve que, dans les versions récentes iOS, UILabel
rend le texte à l'aide de frameworks TextKit/CoreText. Dans les versions antérieures, il utilisait WebKit sous le capot. Qui sait ce qu'il utilisera à l'avenir. Reproduire avec TextKit pose encore des problèmes évidents (ne pas parler de solutions pérennes). Nous ne savons pas vraiment (ou ne pouvons pas garantir) comment la mise en page et le rendu du texte sont réellement effectués, c’est-à-dire quels paramètres sont utilisés - il suffit de regarder tous les cas Edge mentionnés. Certaines solutions peuvent "accidentellement" fonctionner pour vous dans certains cas simples que vous avez testés - cela ne garantit rien, même dans la version actuelle d'iOS. Le rendu du texte est difficile, ne me lancez pas sur le support RTL, Dynamic Type, emojis, etc.
Si vous avez besoin d’une solution appropriée, n’utilisez pas UILabel
. C'est un composant simple et le fait que ses composants internes TextKit ne sont pas exposés dans l'API indique clairement qu'Apple ne veut pas que les développeurs construisent des solutions reposant sur ces détails d'implémentation.
Sinon, vous pouvez utiliser UITextView. Il expose facilement ses composants internes TextKit et est très configurable pour vous permettre d’atteindre presque tout ce que UILabel
est bon.
Vous pouvez également simplement aller plus bas et utiliser TextKit directement (ou même plus bas avec CoreText). Si vous avez réellement besoin de trouver des positions de texte, vous aurez peut-être besoin d'autres outils puissants disponibles dans ces frameworks. Ils sont plutôt difficiles à utiliser au début, mais j’ai le sentiment que la plus grande partie de la complexité provient de l’apprentissage de la manière dont la disposition et le rendu du texte fonctionnent - c’est une compétence très pertinente et utile. Une fois que vous vous serez familiarisé avec les excellents cadres de texte d’Apple, vous vous sentirez en mesure de faire beaucoup plus avec le texte.