web-dev-qa-db-fra.com

Créer UITextRange à partir de NSRange

J'ai besoin de trouver le pixel-frame pour différentes plages dans un textview. J'utilise le - (CGRect)firstRectForRange:(UITextRange *)range; pour le faire. Cependant, je ne trouve pas comment créer une UITextRange.

Fondamentalement, voici ce que je recherche:

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {

    UITextRange*range2 = [UITextRange rangeWithNSRange:range]; //DOES NOT EXIST 
    CGRect rect = [textView firstRectForRange:range2];
    return rect;
}

Apple indique qu'il faut sous-classer UITextRange et UITextPosition pour adopter le protocole UITextInput. Je ne le fais pas, mais j'ai quand même essayé, en suivant l'exemple de code du doc ​​et en passant la sous-classe à firstRectForRange, ce qui a entraîné un crash.

S'il existe un moyen plus facile d'ajouter une UILables de couleur différente à une vue en texte, dites-le-moi. J'ai essayé d'utiliser UIWebView avec content editable défini sur TRUE, mais la communication avec JS ne me passionne pas et la coloration est la seule chose dont j'ai besoin.

Merci d'avance.

46
Johannes Lund

Vous pouvez créer une plage de texte avec la méthode textRangeFromPosition:toPosition. Cette méthode nécessite deux positions. Vous devez donc calculer les positions pour le début et la fin de votre plage. Cela se fait avec la méthode positionFromPosition:offset, qui retourne une position à partir d'une autre position et un offset de caractères.

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
    UITextPosition *beginning = textView.beginningOfDocument;
    UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [textView positionFromPosition:start offset:range.length];
    UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
    CGRect rect = [textView firstRectForRange:textRange];
    return [textView convertRect:rect fromView:textView.textInputView];
}
89

C'est un peu ridicule qui semble être si compliqué. Une solution de contournement simple consiste à sélectionner la plage (accepte NSRange), puis à lire le selectedTextRange (renvoie UITextRange):

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
    textView.selectedRange = range;
    UITextRange *textRange = [textView selectedTextRange]; 
    CGRect rect = [textView firstRectForRange:textRange];
    return rect;
}

Cela a fonctionné pour moi même si textView n'est pas le premier répondant. 


Si vous ne voulez pas que la sélection persiste, vous pouvez réinitialiser selectedRange:

textView.selectedRange = NSMakeRange(0, 0);

... ou enregistrez la sélection actuelle et restaurez-la ensuite

NSRange oldRange = textView.selectedRange;
// do something
// then check if the range is still valid and
textView.selectedRange = oldRange;
17
auco

Pour la question du titre, voici une extension Swift 2 qui crée un UITextRange à partir d’un NSRange.

Le seul initialiseur pour UITextRange est une méthode d'instance sur le protocole UITextInput; l'extension nécessite donc que vous passiez également à UITextInput, tel que UITextField ou UITextView .

extension NSRange {
    func toTextRange(textInput textInput:UITextInput) -> UITextRange? {
        if let rangeStart = textInput.positionFromPosition(textInput.beginningOfDocument, offset: location),
            rangeEnd = textInput.positionFromPosition(rangeStart, offset: length) {
            return textInput.textRangeFromPosition(rangeStart, toPosition: rangeEnd)
        }
        return nil
    }
}
6
Andrew Schreiber

Swift 4 de la réponse d'Andrew Schreiber pour un copier/coller facile

extension NSRange {
    func toTextRange(textInput:UITextInput) -> UITextRange? {
        if let rangeStart = textInput.position(from: textInput.beginningOfDocument, offset: location),
            let rangeEnd = textInput.position(from: rangeStart, offset: length) {
            return textInput.textRange(from: rangeStart, to: rangeEnd)
        }
        return nil
    }
}
5
CodenameDuchess

Swift 4 de la réponse de Nicolas Bachschmidt sous la forme d'une extension UITextView utilisant swifty Range<String.Index> au lieu de NSRange:

extension UITextView {
    func frame(ofTextRange range: Range<String.Index>?) -> CGRect? {
        guard let range = range else { return nil }
        let length = range.upperBound.encodedOffset-range.lowerBound.encodedOffset
        guard
            let start = position(from: beginningOfDocument, offset: range.lowerBound.encodedOffset),
            let end = position(from: start, offset: length),
            let txtRange = textRange(from: start, to: end)
            else { return nil }
        let rect = self.firstRect(for: txtRange)
        return self.convert(rect, to: textInputView)
    }
}

Utilisation possible:

guard let rect = textView.frame(ofTextRange: text.range(of: "awesome")) else { return }
let awesomeView = UIView()
awesomeView.frame = rect.insetBy(dx: -3.0, dy: 0)
awesomeView.layer.borderColor = UIColor.black.cgColor
awesomeView.layer.borderWidth = 1.0
awesomeView.layer.cornerRadius = 3
self.view.insertSubview(awesomeView, belowSubview: textView)
0
Victor