Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adjust Elements after Manipulating String into AttributedString - Swift 4

Tags:

ios

swift

I have the following which allows me to create a bullet list which works really well, however, after the bullet list is created I need to manipulate the outputted Attributed string to have certain elements either in bold or in italics or both.

The function I have is:

@IBOutlet var label: UILabel!
let bulletString = ["String 1","String 2","String 3"]

label.attributedText = label.bulletPoints(stringList: bulletString, font: UIFont.stdFontMediumSeventeen, bullet: "•", lineSpacing: 4, paragraphSpacing: 4, textColor: UIColor.darkGreyColor, bulletColor: UIColor.darkGreyColor)

func bulletPoints(stringList: [String],font: UIFont,bullet: String = "\u{2022}",indentation: CGFloat = 20,lineSpacing: CGFloat = 2,paragraphSpacing: CGFloat = 12,textColor: UIColor = .gray,bulletColor: UIColor = .red) -> NSAttributedString{
    let textAttributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: textColor]
    let bulletAttributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: bulletColor]

    let paragraphStyle = NSMutableParagraphStyle()
    let nonOptions = [NSTextTab.OptionKey: Any]()
    paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: indentation, options: nonOptions)]
    paragraphStyle.defaultTabInterval = indentation
    paragraphStyle.lineSpacing = lineSpacing
    paragraphStyle.paragraphSpacing = paragraphSpacing
    paragraphStyle.headIndent = indentation

    let bulletList = NSMutableAttributedString()
    for string in stringList {
        let formattedString = "\(bullet)\t\(string)\n"
        let attributedString = NSMutableAttributedString(string: formattedString)

        attributedString.addAttributes(
            [NSAttributedStringKey.paragraphStyle : paragraphStyle],
            range: NSMakeRange(0, attributedString.length))

        attributedString.addAttributes(
            textAttributes,
            range: NSMakeRange(0, attributedString.length))

        let string:NSString = NSString(string: formattedString)
        let rangeForBullet:NSRange = string.range(of: bullet)
        attributedString.addAttributes(bulletAttributes, range: rangeForBullet)
        bulletList.append(attributedString)
    }
    return bulletList
}

What I am looking for is a way to pass in a boolean to state if the bullet string requires either bold or italic text and if so what the elements of the intital string are that need this treatment.

The bulletPoints function sits in an extension file and works as expected.

like image 549
Justin Erswell Avatar asked Dec 06 '17 14:12

Justin Erswell


2 Answers

Using a model to link the bold/italic to the strings in question, as Neil suggests, helps you in this case. Here's a version which links font traits to the strings for each bullet, then uses those when building up the string.

I've refactored your bulletPoints function as well, to remove the use of ranges and simplify it a little. It could stay in an extension (I assume you have it on UILabel?) but there's no reason for it to, since it returns the string anyway. I've written it as a function which could be used in any class

class ViewController: UIViewController {

    @IBOutlet var label: UILabel!


    override func viewWillAppear(_ animated: Bool) {
        let bulletStrings = [BulletString(string: "String 1", traits: []),
                             BulletString(string: "String 2", traits: [.traitBold]),
                             BulletString(string: "String 3", traits: [.traitItalic]),
                             BulletString(string: "String 4", traits: [.traitBold, .traitItalic])]
        label.attributedText = bulletPoints(stringList: bulletStrings, font: UIFont.systemFont(ofSize: 15.0), bullet: "•", lineSpacing: 4, paragraphSpacing: 4, textColor: UIColor.darkGray, bulletColor: UIColor.darkGray)

    }

    func bulletPoints(stringList: [BulletString],
                      font: UIFont,
                      bullet: String = "\u{2022}",
                      indentation: CGFloat = 20,
                      lineSpacing: CGFloat = 2,
                      paragraphSpacing: CGFloat = 12,
                      textColor: UIColor = .gray,
                      bulletColor: UIColor = .red) -> NSAttributedString {

        let bulletList = NSMutableAttributedString()
        for bulletString in stringList {
            let attributedString = NSMutableAttributedString(string: "")
            let bulletAttributes: [NSAttributedStringKey: Any] = [
                .foregroundColor: bulletColor,
                .font: font]
            attributedString.append(NSAttributedString(string: bullet, attributes: bulletAttributes))

            let textAttributes: [NSAttributedStringKey: Any] = [
                .font: font.withTraits(traits: bulletString.traits),
                .foregroundColor: textColor,
                .paragraphStyle : paragraphStyle(indentation: indentation, lineSpacing: lineSpacing, paragraphSpacing: paragraphSpacing)
            ]
            attributedString.append(NSAttributedString(string:"\t\(bulletString.string)\n", attributes: textAttributes))

            bulletList.append(attributedString)
        }
        return bulletList
    }

    private func paragraphStyle(indentation: CGFloat, lineSpacing: CGFloat, paragraphSpacing: CGFloat) -> NSParagraphStyle {
        let style = NSMutableParagraphStyle()
        let nonOptions = [NSTextTab.OptionKey: Any]()
        style.tabStops = [NSTextTab(textAlignment: .left, location: indentation, options: nonOptions)]
        style.defaultTabInterval = indentation
        style.lineSpacing = lineSpacing
        style.paragraphSpacing = paragraphSpacing
        style.headIndent = indentation
        return style
    }
}

struct BulletString {
    let string: String
    let traits: UIFontDescriptorSymbolicTraits
}

extension UIFont {

    func withTraits(traits:UIFontDescriptorSymbolicTraits...) -> UIFont {
        let descriptor = self.fontDescriptor
            .withSymbolicTraits(UIFontDescriptorSymbolicTraits(traits))!
        return UIFont(descriptor: descriptor, size: 0)
    }

}

Results of example

If you wanted to have the bullets match the style of their strings, i.e. be bolded or italicised, you could just add the attributes in a single pass for each bullet

like image 64
Josh Heald Avatar answered Oct 23 '22 04:10

Josh Heald


You can achieve it using multiple fonts and text ranges. If you know the ranges of the text on which you want to apply multiple styles, you can just use fonts. Check the below example.

let fullString = "Bold normal italic"
let attrString = NSMutableAttributedString(string: fullString, attributes: [.font: UIFont.systemFont(ofSize: 18.0)])

let range1 = (fullString as NSString).range(of: "Bold")
let range2 = (fullString as NSString).range(of: "italic")
attrString.addAttributes([.font: UIFont.boldSystemFont(ofSize: 20.0)], range: range1)
attrString.addAttributes([.font: UIFont.boldSystemFont(ofSize: 20.0).italics()], range: range2)

label.attributedText = attrString

Whereas I use simple extension for UIFont.

extension UIFont {
    func withTraits(_ traits: UIFontDescriptorSymbolicTraits) -> UIFont {
        if let fd = fontDescriptor.withSymbolicTraits(traits) {
            return UIFont(descriptor: fd, size: pointSize)
        }
        return self
    }
    
    func italics() -> UIFont {
        return withTraits(.traitItalic)
    }
}

So basically, what you need to know is, which text should be marked as italic, bold and normal. Afterwards just calculate the ranges for those texts in your original text using NSString.range(of: ) and update the attributes appropriately.

Note: You can also calculate the range using start and endIndex. For reference check this SO answer.

like image 37
Radim Halfar Avatar answered Oct 23 '22 04:10

Radim Halfar