26

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.

lostInTransit
  • 70,519
  • 61
  • 198
  • 274

8 Answers8

31

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

Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
Aruna Mudnoor
  • 4,795
  • 14
  • 16
6

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
Zack Shapiro
  • 6,648
  • 17
  • 83
  • 151
Adam Neuwirth
  • 529
  • 5
  • 10
5

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;
}
CTiPKA
  • 2,944
  • 1
  • 24
  • 27
5

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
Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
4

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
Geoff H
  • 3,107
  • 1
  • 28
  • 53
  • Using this in a stackview for some reason does not work on pre 15.4 devices and emulators. Sometimes the links are correctly attributed and other times the entire string is attributed. haphazard functionality. 15.4 however seems to work as expected of course. – DevinM Apr 13 '22 at 15:10
0

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];
huong
  • 4,534
  • 6
  • 33
  • 54
-1

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
ingconti
  • 10,876
  • 3
  • 61
  • 48
-1

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
possen
  • 8,596
  • 2
  • 39
  • 48
  • The problem with this is more often than not, a localized version of it will need to change the structure of the sentence and your approach breaks. – Rafael Nobre Aug 11 '22 at 22:57