How do I make NSMutableAttributedString work for links? - swift

I'm using NSMutableAttributedString to format text strings. It works fine, even with the links. But for some reason the font size for the links won't change even though I have specified the size. My function looks like this: (There are comments in the code to explain everything)
func formatfunc2(chapter: String, boldStart: Int, boldLength: Int, italicsStart: Int, italicsLength: Int, link: [String], linkStart: [Int], linkEnd: [Int]) -> NSAttributedString {
let bold = UIFont.boldSystemFont(ofSize: 17)
let italics = UIFont.italicSystemFont(ofSize: 17)
//BELOW IS MY FORMAT FOR THE HYPERLINK
let hyperlink = UIFont.boldSystemFont(ofSize: 17)
let attributedString = NSMutableAttributedString.init(string: chapter, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17)])
//BELOW ARE FORMAT FOR BOLD AND ITALICS - AND THEY WORK FINE
attributedString.addAttribute(.font, value: bold, range: NSRange.init(location: boldStart, length: boldLength))
attributedString.addAttribute(.font, value: italics, range: NSRange.init(location: italicsStart, length: italicsLength))
//THE LOOP GOES THROUGH ALL LINK ADRESSES AND THEIR POSITIONS
for i in 0...link.count - 1 {
//HERE I ADD THE FONT SIZE TO THE SAME POSITION AS THE LINKS, BUT IT DOESN'T WORK
attributedString.addAttribute(.font, value: hyperlink, range: NSRange.init(location: linkStart[i], length: linkEnd[i]))
let url = URL(string: link[i]) as! URL
//MY THEORY IS THAT THE CODE BELOW OVERRIDES THE PREVIOUS FONT SIZE
attributedString.setAttributes([.link: url], range: NSMakeRange(linkStart[i], linkEnd[i]))
}
return attributedString
}
So the format works fine for all other text but not for the links. Can I add font size in the last code part instead? :
attributedString.setAttributes([.link: url], range: NSMakeRange(linkStart[i], linkEnd[i]))

Related

NSTextAttachment image in attributed text with foreground colour

When I add an image attachment to an UITextView with a foreground colour set, the image is blanked out with the set colour:
let attrString = NSMutableAttributedString(string: rawText, attributes: [.font: UIFont.systemFont(ofSize: 17), .foregroundColor: UIColor.black])
let attachment = NSTextAttachment(image: image)
let imgStr = NSMutableAttributedString(attachment: attachment)
attrString.append(imgStr)
textview.attributedText = attrString
When I removed .foregroundColor: UIColor.black, the image is displayed correctly, but I need to be able to set the attributed text colour.
I tried to explicitly remove the .foregroundColor attribute after adding the image attachment with no luck. I also tried to remove the .foregroundColor attribute from most of the text and it still wouldn't work, only removing the attribute from the entire string works:
attrString.removeAttribute(.foregroundColor, range: NSRange(location: attrString.length-1, length: 1)) // does not work
// -------
attrString.removeAttribute(.foregroundColor, range: NSRange(location: 1, length: attrString.length-1)) // does not work
// -------
attrString.removeAttribute(.foregroundColor, range: NSRange(location: 0, length: attrString.length)) // works but no text colour
This is developed on Xcode 11.0, iOS 13. Is this a UITextView/iOS bug or an expected behaviour (which, I don't think is likely)? How do I display the image correctly with the text colour set?
It looks like there is a bug with the NSTextAttachment(image:) constructor (on iOS 13, at the time of this answer), the following image attachment construction works correctly:
let attachment = NSTextAttachment()
attachment.image = image

Fontsize of attributedString doesn't change

I have an attributedString and want to change only it's fontsize. To do that, I use another method that I found on StackOverflow. For most cases, this is working, but somehow it doesn't change the whole attributedString in one case.
Method to change the size:
/**
*A struct with static methods that can be useful for your GUI
*/
struct GuiUtils {
static func setAttributedStringToSize(attributedString: NSAttributedString, size: CGFloat) -> NSMutableAttributedString {
let mus = NSMutableAttributedString(attributedString: attributedString)
mus.enumerateAttribute(.font, in: NSRange(location: 0, length: mus.string.count)) { (value, range, stop) in
if let oldFont = value as? UIFont {
let newFont = oldFont.withSize(size)
mus.addAttribute(.font, value: newFont, range: range)
}
}
return mus
}
}
Working:
label.attributedText = GuiUtils.setAttributedStringToSize(attributedString: attributedString, size: fontSize)
Not working:
mutableAttributedString.replaceCharacters(in: gapRange, with: filledGap)
label.attributedText = GuiUtils.setAttributedStringToSize(attributedString: mutableAttributedString.replaceCharacters, size: fontSize)
Somehow, the replaced text does not change its size.
Excuse me, but do you sure that your filledGap attributed string has font attribute? Because if it doesn't – this part will not be handled by the enumerateAttribute block.
In this case your fix will be just to set any font to the whole filledGap string, to be sure that it's part will be handled by the enumerateAttribute block.

How to make the text as a hyperlink in NSTextView programmatically? Swift 4, Xcode 9.4

How to make the text as a hyperlink in NSTextView programmatically?
Like this:
Just click here to register
or like this:
Just http://example.com to register
I found this solution but it works only for iOS, not for macOS
Try this:
let attributedString = NSMutableAttributedString(string: "Just click here to register")
let range = NSRange(location: 5, length: 10)
let url = URL(string: "https://www.apple.com")!
attributedString.setAttributes([.link: url], range: range)
textView.textStorage?.setAttributedString(attributedString)
// Define how links should look like within the text view
textView.linkTextAttributes = [
.foregroundColor: NSColor.blue,
.underlineStyle: NSUnderlineStyle.styleSingle.rawValue
]
If you needed set Font Size for links use this ->
let input = "Your string with urls"
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
let attributes = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 15)]
let attributedString = NSMutableAttributedString(string: input, attributes: attributes)
if matches.count > 0{
for match in matches {
guard let range = Range(match.range, in: input) else { continue }
let url = input[range]
attributedString.setAttributes([.link: url, .font: NSFont.systemFont(ofSize: 15)], range: match.range)
}
}
youTextView.textStorage?.setAttributedString(attributedString)
Swift 5 / macOS 12 solution to make the link clickable and have the same font any NSTextField's you display in the same window:
Define some NSTextView IBOutlet (mine is called tutorialLink):
#IBOutlet weak var tutorialLink : NSTextView!
Then here's the styling code in one function:
fileprivate func initTutorialLink() {
let attributedString = NSMutableAttributedString(string: "Here's a tutorial on how to set up a Digital Ocean S3 account.")
// Set font for entire string
let fontAttributes = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13)]
let range2 = NSRange(location: 0, length: 61)
attributedString.setAttributes(fontAttributes, range: range2)
// Set url and same font for just the link range
let range = NSRange(location: 21, length: 40)
let url = URL(string: tutorialUrl)!
attributedString.setAttributes([.link: url, NSAttributedString.Key.font:NSFont.systemFont(ofSize: 13)], range: range)
// Store the attributed string
tutorialLink.textStorage?.setAttributedString(attributedString)
// Mark how link text should be colored and underlined
tutorialLink.linkTextAttributes = [
.foregroundColor: NSColor.systemBlue,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
// Give non-linked text same color as other labels
tutorialLink.textColor = .labelColor
}
In IB, you obviously need to connect your NSTextView (called Scrollable Text View in the Objects Library) to the IBOutlet. Less obviously, you need to click the box to make the NSTextView Selectable. I don't know why exactly, but if it's not Selectable then your link will not react when clicked.

NSAttributedString, change the font overall BUT keep all other attributes?

Say I have an NSMutableAttributedString .
The string has a varied mix of formatting throughout:
Here is an example:
This string is hell to change in iOS, it really sucks.
However, the font per se is not the font you want.
I want to:
for each and every character, change that character to a specific font (say, Avenir)
BUT,
for each and every character, keep the mix of other attributions (bold, italic, colors, etc etc) which was previously in place on that character.
How the hell do you do this?
Note:
if you trivially add an attribute "Avenir" over the whole range: it simply deletes all the other attribute ranges, you lose all formatting. Unfortunately, attributes are not, in fact "additive".
Since rmaddy's answer did not work for me (f.fontDescriptor.withFace(font.fontName) does not keep traits like bold), here is an updated Swift 4 version that also includes color updating:
extension NSMutableAttributedString {
func setFontFace(font: UIFont, color: UIColor? = nil) {
beginEditing()
self.enumerateAttribute(
.font,
in: NSRange(location: 0, length: self.length)
) { (value, range, stop) in
if let f = value as? UIFont,
let newFontDescriptor = f.fontDescriptor
.withFamily(font.familyName)
.withSymbolicTraits(f.fontDescriptor.symbolicTraits) {
let newFont = UIFont(
descriptor: newFontDescriptor,
size: font.pointSize
)
removeAttribute(.font, range: range)
addAttribute(.font, value: newFont, range: range)
if let color = color {
removeAttribute(
.foregroundColor,
range: range
)
addAttribute(
.foregroundColor,
value: color,
range: range
)
}
}
}
endEditing()
}
}
Or, if your mix-of-attributes does not include font,
then you don't need to remove old font:
let myFont: UIFont = .systemFont(ofSize: UIFont.systemFontSize);
myAttributedText.addAttributes(
[NSAttributedString.Key.font: myFont],
range: NSRange(location: 0, length: myAttributedText.string.count));
Notes
The problem with f.fontDescriptor.withFace(font.fontName) is that it removes symbolic traits like italic, bold or compressed, since it will for some reason override those with default traits of that font face. Why this is so totally eludes me, it might even be an oversight on Apple's part; or it's "not a bug, but a feature", because we get the new font's traits for free.
So what we have to do is create a font descriptor that has the symbolic traits from the original font's font descriptor: .withSymbolicTraits(f.fontDescriptor.symbolicTraits). Props to rmaddy for the initial code on which I iterated.
I've already shipped this in a production app where we parse a HTML string via NSAttributedString.DocumentType.html and then change the font and color via the extension above. No problems so far.
Here is a much simpler implementation that keeps all attributes in place, including all font attributes except it allows you to change the font face.
Note that this only makes use of the font face (name) of the passed in font. The size is kept from the existing font. If you want to also change all of the existing font sizes to the new size, change f.pointSize to font.pointSize.
extension NSMutableAttributedString {
func replaceFont(with font: UIFont) {
beginEditing()
self.enumerateAttribute(.font, in: NSRange(location: 0, length: self.length)) { (value, range, stop) in
if let f = value as? UIFont {
let ufd = f.fontDescriptor.withFamily(font.familyName).withSymbolicTraits(f.fontDescriptor.symbolicTraits)!
let newFont = UIFont(descriptor: ufd, size: f.pointSize)
removeAttribute(.font, range: range)
addAttribute(.font, value: newFont, range: range)
}
}
endEditing()
}
}
And to use it:
let someMutableAttributedString = ... // some attributed string with some font face you want to change
someMutableAttributedString.replaceFont(with: UIFont.systemFont(ofSize: 12))
my two cents for OSX/AppKit>
extension NSAttributedString {
// replacing font to all:
func setFont(_ font: NSFont, range: NSRange? = nil)-> NSAttributedString {
let mas = NSMutableAttributedString(attributedString: self)
let range = range ?? NSMakeRange(0, self.length)
mas.addAttributes([.font: font], range: range)
return NSAttributedString(attributedString: mas)
}
// keeping font, but change size:
func setFont(size: CGFloat, range: NSRange? = nil)-> NSAttributedString {
let mas = NSMutableAttributedString(attributedString: self)
let range = range ?? NSMakeRange(0, self.length)
mas.enumerateAttribute(.font, in: range) { value, range, stop in
if let font = value as? NSFont {
let name = font.fontName
let newFont = NSFont(name: name, size: size)
mas.addAttributes([.font: newFont!], range: range)
}
}
return NSAttributedString(attributedString: mas)
}
Important -
rmaddy has invented an entirely new technique for this annoying problem in iOS.
The answer by manmal is the final perfected version.
Purely for the historical record here is roughly how you'd go about doing it the old days...
// carefully convert to "our" font - "re-doing" any other formatting.
// change each section BY HAND. total PITA.
func fixFontsInAttributedStringForUseInApp() {
cachedAttributedString?.beginEditing()
let rangeAll = NSRange(location: 0, length: cachedAttributedString!.length)
var boldRanges: [NSRange] = []
var italicRanges: [NSRange] = []
var boldANDItalicRanges: [NSRange] = [] // WTF right ?!
cachedAttributedString?.enumerateAttribute(
NSFontAttributeName,
in: rangeAll,
options: .longestEffectiveRangeNotRequired)
{ value, range, stop in
if let font = value as? UIFont {
let bb: Bool = font.fontDescriptor.symbolicTraits.contains(.traitBold)
let ii: Bool = font.fontDescriptor.symbolicTraits.contains(.traitItalic)
// you have to carefully handle the "both" case.........
if bb && ii {
boldANDItalicRanges.append(range)
}
if bb && !ii {
boldRanges.append(range)
}
if ii && !bb {
italicRanges.append(range)
}
}
}
cachedAttributedString!.setAttributes([NSFontAttributeName: font_f], range: rangeAll)
for r in boldANDItalicRanges {
cachedAttributedString!.addAttribute(NSFontAttributeName, value: font_fBOTH, range: r)
}
for r in boldRanges {
cachedAttributedString!.addAttribute(NSFontAttributeName, value: font_fb, range: r)
}
for r in italicRanges {
cachedAttributedString!.addAttribute(NSFontAttributeName, value: font_fi, range: r)
}
cachedAttributedString?.endEditing()
}
.
Footnote. Just for clarity on a related point. This sort of thing inevitably starts as a HTML string. Here's a note on how to convert a string that is html to an NSattributedString .... you will end up with nice attribute ranges (italic, bold etc) BUT the fonts will be fonts you don't want.
fileprivate extension String {
func htmlAttributedString() -> NSAttributedString? {
guard let data = self.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
guard let html = try? NSMutableAttributedString(
data: data,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil) else { return nil }
return html
}
}
.
Even that part of the job is non-trivial, it takes some time to process. In practice you have to background it to avoid flicker.
Obj-C version of #manmal's answer
#implementation NSMutableAttributedString (Additions)
- (void)setFontFaceWithFont:(UIFont *)font color:(UIColor *)color {
[self beginEditing];
[self enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, self.length)
options:0
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
UIFont *oldFont = (UIFont *)value;
UIFontDescriptor *newFontDescriptor = [[oldFont.fontDescriptor fontDescriptorWithFamily:font.familyName] fontDescriptorWithSymbolicTraits:oldFont.fontDescriptor.symbolicTraits];
UIFont *newFont = [UIFont fontWithDescriptor:newFontDescriptor size:font.pointSize];
if (newFont) {
[self removeAttribute:NSFontAttributeName range:range];
[self addAttribute:NSFontAttributeName value:newFont range:range];
}
if (color) {
[self removeAttribute:NSForegroundColorAttributeName range:range];
[self addAttribute:NSForegroundColorAttributeName value:newFont range:range];
}
}];
[self endEditing];
}
#end
There is a tiny bug in the accepted answer causing the original font size to get lost.
To fix this simply replace
font.pointSize
with
f.pointSize
This ensures that e.g. H1 and H2 headings have the correct size.
Swift 5
This properly scales the font size, as other answers overwrite the font size, which may differ, like with sub, sup attribute
For iOS replace NSFont->UIFont and NSColor->UIColor
extension NSMutableAttributedString {
func setFont(_ font: NSFont, textColor: NSColor? = nil) {
guard let fontFamilyName = font.familyName else {
return
}
beginEditing()
enumerateAttribute(NSAttributedString.Key.font, in: NSMakeRange(0, length), options: []) { (value, range, stop) in
if let oldFont = value as? NSFont {
let descriptor = oldFont
.fontDescriptor
.withFamily(fontFamilyName)
.withSymbolicTraits(oldFont.fontDescriptor.symbolicTraits)
// Default font is always Helvetica 12
// See: https://developer.apple.com/documentation/foundation/nsattributedstring?language=objc
let size = font.pointSize * (oldFont.pointSize / 12)
let newFont = NSFont(descriptor: descriptor, size: size) ?? oldFont
addAttribute(NSAttributedString.Key.font, value: newFont, range: range)
}
}
if let textColor = textColor {
addAttributes([NSAttributedString.Key.foregroundColor:textColor], range: NSRange(location: 0, length: length))
}
endEditing()
}
}
extension NSAttributedString {
func settingFont(_ font: NSFont, textColor: NSColor? = nil) -> NSAttributedString {
let ms = NSMutableAttributedString(attributedString: self)
ms.setFont(font, textColor: textColor)
return ms
}
}
Would it be valid to let a UITextField do the work?
Like this, given attributedString and newfont:
let textField = UITextField()
textField.attributedText = attributedString
textField.font = newFont
let resultAttributedString = textField.attributedText
Sorry, I was wrong, it keeps the "Character Attributes" like NSForegroundColorAttributeName, e.g. the colour, but not the UIFontDescriptorSymbolicTraits, which describe bold, italic, condensed, etc.
Those belong to the font and not the "Character Attributes". So if you change the font, you are changing the traits as well. Sorry, but my proposed solution does not work. The target font needs to have all traits available as the original font for this to work.

Swift UITextView with different formatting

Sorry for a basic question, but I'm not sure where to start. Is the following possible in Swift?
In a UITextView (Not a label, as in the possible duplicate), different bits of text having different formatting: for instance, a line of large text in the same UITextView as a line of small text. Here is a mockup of what I mean:
Is this possible in one UITextView?
You should have a look at NSAttributedString. Here's an example of how you could use it:
let largeTextString = "Here is some large, bold text"
let smallTextString = "Here is some smaller text"
let textString = "\n\(largeTextString)\n\n\(smallTextString)"
let attrText = NSMutableAttributedString(string: textString)
let largeFont = UIFont(name: "Arial-BoldMT", size: 50.0)!
let smallFont = UIFont(name: "Arial", size: 30.0)!
// Convert textString to NSString because attrText.addAttribute takes an NSRange.
let largeTextRange = (textString as NSString).range(of: largeTextString)
let smallTextRange = (textString as NSString).range(of: smallTextString)
attrText.addAttribute(NSFontAttributeName, value: largeFont, range: largeTextRange)
attrText.addAttribute(NSFontAttributeName, value: smallFont, range: smallTextRange)
textView.attributedText = attrText
The result: