lostInTransit
lostInTransit

Reputation: 70997

UITextView linkTextAttributes font attribute not applied to NSAttributedString

I have an NSAttributedString generated from HTML which includes some links. The attributed string is shown in a UITextView. I wish to apply a different font style for the links and am setting linkTextAttributes for this. I've added NSForegroundColorAttributeName, NSFontAttributeName and NSUnderlineStyleAttributeName. For some reason the foreground color is applied but the remaining attributes are not.

myTextView.linkTextAttributes = [NSForegroundColorAttributeName : UIColor.redColor(), NSFontAttributeName : textLinkFont, NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleNone.rawValue]

Has anyone else faced this and how do I change the font style for links without having to apply in-line CSS to the original HTML? Thanks.

Upvotes: 27

Views: 19685

Answers (8)

possen
possen

Reputation: 9266

Since attributed strings are generally a pain, I find it is better to avoid the range APIs, and to keep things as immutable as possible. Set the attributes when you create the attributed string rather than going back and trying to set a range. This will also help with localization because figuring out ranges for different languages is quite tricky (the sample below does not show localization to keep things illustrative). It makes things cleaner and easier to follow. When all strings are constructed, assemble the whole thing from the pieces.

// build string
let intro = NSAttributedString(string: "I agree that I have read and understood the ")
let terms = NSAttributedString(string: "Terms and Conditions ", attributes: [.link: "https://apple.com" as Any])
let middle = NSAttributedString(string: "and ")
let privacy = NSAttributedString(string: "Privacy Policy. ", attributes: [.link: "https://example.com" as Any])
let ending = NSAttributedString(string: "This application may send me SMS messages.")
let attrStr = NSMutableAttributedString()
attrStr.append(intro)
attrStr.append(terms)
attrStr.append(middle)
attrStr.append(privacy)
attrStr.append(ending)

// set the link color
let linkAttributes: [NSAttributedString.Key: AnyObject] = [.foregroundColor: UIColor(named: "Secondary")!]
textView.linkTextAttributes = linkAttributes
textView.attributedText = attrStr

Upvotes: -1

Geoff H
Geoff H

Reputation: 3247

Swift 5 version of Ryan Heitner's awesome answer:

guard let attributedString = textView.attributedText else { return }
guard let linkFont = UIFont(name: "HelveticaNeue-Bold", size: 20.0) else { return }

let newString = NSMutableAttributedString(attributedString: attributedString)
let types: NSTextCheckingResult.CheckingType = [.link, .phoneNumber]

guard let linkDetector = try? NSDataDetector(types: types.rawValue) else { return }
let range = NSRange(location: 0, length: attributedString.length)

linkDetector.enumerateMatches(in: attributedString.string, options: [], range: range, using: { (match: NSTextCheckingResult?, flags: NSRegularExpression.MatchingFlags, stop) in
    if let matchRange = match?.range {
        newString.removeAttribute(NSAttributedString.Key.font, range: matchRange)
        newString.addAttribute(NSAttributedString.Key.font, value: linkFont, range: matchRange)
    }
})

textView.attributedText = newString

Upvotes: 4

Adam Neuwirth
Adam Neuwirth

Reputation: 537

Updated for Swift 4:

let originalText = NSMutableAttributedString(attributedString: textView.attributedText)
var newString = NSMutableAttributedString(attributedString: textView.attributedText)

originalText.enumerateAttributes(in: NSRange(0..<originalText.length), options: .reverse) { (attributes, range, pointer) in
    if let _ = attributes[NSAttributedString.Key.link] {
        newString.removeAttribute(NSAttributedString.Key.font, range: range)
        newString.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 30), range: range)
    }
}

self.textView.attributedText = newString // updates the text view on the vc

Upvotes: 6

CTiPKA
CTiPKA

Reputation: 2974

For some reason postprocessing attributed string with enumerateAttributesInRange: do not work for me.

So I used NSDataDetector to detect link and enumerateMatchesInString:options:range:usingBlock: to put my style for all links in string. Here is my processing function:

+ (void) postProcessTextViewLinksStyle:(UITextView *) textView {
   NSAttributedString *attributedString = textView.attributedText;
   NSMutableAttributedString *attributedStringWithItalicLinks = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];

   NSError *error = nil;
   NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink
                                                           error:&error];

   [detector enumerateMatchesInString:[attributedString string]
                           options:0
                             range:NSMakeRange(0, [attributedString length])
                        usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop){
                            NSRange matchRange = [match range];
                            NSLog(@"Links style postprocessing. Range (from: %lu, length: %lu )", (unsigned long)matchRange.location, (unsigned long)matchRange.length);
                            if ([match resultType] == NSTextCheckingTypeLink) {                                    
                                [attributedStringWithItalicLinks removeAttribute:NSFontAttributeName range:matchRange];
                                [attributedStringWithItalicLinks addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"YourFont-Italic" size:14.0f] range:matchRange];
                            }
                        }];

   textView.attributedText = attributedStringWithItalicLinks;
}

Upvotes: 5

ingconti
ingconti

Reputation: 11646

for simple cases: (without horrible HTML use):

    let linkTextAttributes : [String : Any] = [
        NSForegroundColorAttributeName: UIColor.red,
        NSUnderlineColorAttributeName: UIColor.magenta,
        NSUnderlineStyleAttributeName: NSUnderlineStyle.patternSolid.rawValue
    ]

    self.infoText.linkTextAttributes = linkTextAttributes

Upvotes: -1

Ryan Heitner
Ryan Heitner

Reputation: 13632

This is a swift 3 update of answer above from @Arun Ammannaya

guard let font = UIFont.init(name: "Roboto-Regular", size: 15) else {
    return
}
let newString = NSMutableAttributedString(attributedString: string)
let range = NSRange(location:0,length: string.length)
string.enumerateAttributes(in: range, options: .reverse, using: { (attributes : [String : Any], range : NSRange, _) -> Void in
    if let _ = attributes[NSLinkAttributeName] {
        newString.removeAttribute(NSFontAttributeName, range: range)
        newString.addAttribute(NSFontAttributeName, value: font, range: range)
    }
})
errorTextView.attributedText = newString
errorTextView.linkTextAttributes = [NSForegroundColorAttributeName : UIColor.green, NSUnderlineStyleAttributeName : NSUnderlineStyle.styleSingle.rawValue]

This is a Swift 3 solution to @CTiPKA which I prefer since it avoids HTML

guard let attributedString = errorTextView.attributedText else {
    return
}
guard let font = UIFont.init(name: "Roboto-Regular", size: 15) else {
   return
}
let newString = NSMutableAttributedString(attributedString: attributedString)

let types: NSTextCheckingResult.CheckingType = [.link, .phoneNumber]

guard let linkDetector = try? NSDataDetector(types: types.rawValue) else { return  }
let range = NSRange(location:0,length: attributedString.length)

linkDetector.enumerateMatches(in: attributedString.string, options: [], range: range, using: { (match : NSTextCheckingResult?,
    flags : NSRegularExpression.MatchingFlags, stop) in

    if let matchRange = match?.range {
        newString.removeAttribute(NSFontAttributeName, range: matchRange)
        newString.addAttribute(NSFontAttributeName, value: font, range: matchRange)
    }
})
errorTextView.attributedText = newString

Upvotes: 5

Aruna Mudnoor
Aruna Mudnoor

Reputation: 4825

Not sure why linkTextAttributes doesn't work for the font name. But we can achieve this by updating the link attributes of the NSAttributedString. Check the code below.

        do {
        let htmlStringCode = "For more info <a href=\"http://www.samplelink.com/subpage.php?id=8\">Click here</a>"

        let string = try NSAttributedString(data: htmlStringCode.dataUsingEncoding(NSUTF8StringEncoding)!, options: [NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: NSUTF8StringEncoding], documentAttributes: nil)

        let newString = NSMutableAttributedString(attributedString: string)
        string.enumerateAttributesInRange(NSRange.init(location: 0, length: string.length), options: .Reverse) { (attributes : [String : AnyObject], range:NSRange, _) -> Void in
            if let _ = attributes[NSLinkAttributeName] {
                newString.removeAttribute(NSFontAttributeName, range: range)
                newString.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(30), range: range)
            }
        }
        textField.attributedText = newString
        textField.linkTextAttributes = [NSForegroundColorAttributeName : UIColor.redColor(), NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleNone.rawValue]

    }catch {
    }

This is the objective-C code for this:

NSDictionary *options = @{NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType};
NSData *data = [html dataUsingEncoding:NSUnicodeStringEncoding allowLossyConversion:NO];

NSAttributedString *attributedString = [[NSAttributedString alloc] initWithData:data options:options documentAttributes:nil error:nil];
NSMutableAttributedString *attributedStringWithBoldLinks = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString];

[attributedString enumerateAttributesInRange:NSMakeRange(0, attributedString.string.length) options:NSAttributedStringEnumerationReverse usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {

    if ([attrs objectForKey:NSLinkAttributeName]) {
        [attributedStringWithBoldLinks removeAttribute:NSFontAttributeName range:range];
        [attributedStringWithBoldLinks addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"YourFont-Bold" size:16.0] range:range];
    }
}];

self.linkTextAttributes = @{NSForegroundColorAttributeName : [UIColor redColor]};

self.attributedText = attributedStringWithBoldLinks;

Screenshot

Upvotes: 32

huong
huong

Reputation: 4564

There's also an easy way to apply style for the text if you use html - you can just add the style within the html code. Then you wouldn't need to worry about setting attributes for the text. For example:

NSString *html = [NSString stringWithFormat:@"<p style=\"font-family: Your-Font-Name; color: #344052; font-size: 15px\"><a style=\"color: #0A9FD2\" href=\"https://examplelink.com\">%@</a> %@ on %@</p>", name, taskName, timeString];
NSDictionary *options = @{NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType};
NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];

NSAttributedString *attributedString = [[NSAttributedString alloc] initWithData:data options:options documentAttributes:nil error:nil];

Upvotes: 0

Related Questions