web-dev-qa-db-fra.com

Comment localiser le CGRect pour une sous-chaîne de texte dans un UILabel?

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".

Visual description of problem

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:

  • J'espérais qu'avec iOS 7, un peu de Text Kit résoudrait ce problème, mais la plupart des exemples que j'ai vus avec Text Kit se concentrent sur UITextView et UITextField, plutôt que UILabel.
  • J'ai vu une autre question sur Stack Overflow qui promet de résoudre le problème, mais la réponse acceptée date de plus de deux ans et le code ne fonctionne pas bien avec du texte attribué.

Je parierais que la bonne réponse à cette question implique l'un des éléments suivants:

  • Utilisation d'une méthode de kit texte standard pour résoudre ce problème dans une seule ligne de code. Je parie que cela impliquerait NSLayoutManager et textContainerForGlyphAtIndex:effectiveRange
  • L'écriture d'une méthode complexe qui divise UILabel en lignes et trouve le rect d'un glyphe dans une ligne, probablement à l'aide des méthodes Core Text. Mon meilleur choix actuel est de prendre @ mattt's excellent TTTAttributedLabel , qui utilise une méthode qui trouve un glyphe en un point - si je l'inverse et trouve le point d'un glyphe, cela pourrait fonctionner.

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

67
bryanjclark

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];
}
87
Luke Rogers

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)
16
NoodleOfDeath

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.

11
Joshua

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)
}
}
2
Dima

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)
    }
2
Nico Achkasov

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. 

1
pickwick

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 .

0
Hemang

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.

0
Max O