I want to have one string with different paragraphs styles. The goal is to customize the paragraph/line spacing for different parts of the string. I researched and found this answer but since I added multiple new line characters, not sure how to implement.
Design
This is my goal in terms of layout:
Code
This is the code I have which makes it look like the left image above. Please see the comments Not working in the code. Notice how the spacing is set for the main string, but the other strings can't then set their own custom spacing:
struct BookModel: Codable {
let main: String
let detail: String
}
func createAttributedString(for model: BookModel) -> NSMutableAttributedString {
let fullString = NSMutableAttributedString()
let mainString = NSMutableAttributedString(string: model.main)
let mainStringParagraphStyle = NSMutableParagraphStyle()
mainStringParagraphStyle.alignment = .center
mainStringParagraphStyle.lineSpacing = 10
mainStringParagraphStyle.paragraphSpacing = 30
let mainStringAttributes: [NSAttributedString.Key: Any] = [.paragraphStyle: mainStringParagraphStyle]
let spacingAfterQuote = NSMutableAttributedString(string: "\n")
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-image"))
let lineImageString = NSMutableAttributedString(attachment: lineImageAttachment)
let lineParagraphStyle = NSMutableParagraphStyle()
lineParagraphStyle.alignment = .left
lineParagraphStyle.lineSpacing = 0 // Not working - instead of 0 it is 30 from `mainStringParagraphStyle`
lineParagraphStyle.paragraphSpacing = 0 // Not working - instead of 0 it is 30 from `mainStringParagraphStyle`
let lineAttributes: [NSAttributedString.Key: Any] = [.paragraphStyle: lineParagraphStyle]
let spacingAfterSeparator = NSMutableAttributedString(string: "\n")
let spacingAfterSeparatorParagraphStyle = NSMutableParagraphStyle()
spacingAfterSeparatorParagraphStyle.alignment = .left
spacingAfterSeparatorParagraphStyle.lineSpacing = 0 // Not working - instead of 0 it is 30 from `mainStringParagraphStyle`
spacingAfterSeparatorParagraphStyle.paragraphSpacing = 5 // Not working - instead of 5 it is 30 from `mainStringParagraphStyle`
let spacingAfterSeparatorAttributes: [NSAttributedString.Key: Any] = [.paragraphStyle: spacingAfterSeparatorParagraphStyle]
let detailString = NSMutableAttributedString(string: model.detail)
let detailStringAttributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 20)]
fullString.append(mainString)
fullString.append(spacingAfterQuote)
fullString.append(lineImageString)
fullString.append(spacingAfterSeparator)
fullString.append(detailString)
fullString.addAttributes(mainStringAttributes, range: fullString.mutableString.range(of: model.main))
fullString.addAttributes(lineAttributes, range: fullString.mutableString.range(of: lineImageString.string))
fullString.addAttributes(spacingAfterSeparatorAttributes, range: fullString.mutableString.range(of: spacingAfterSeparator.string))
fullString.addAttributes(detailStringAttributes, range: fullString.mutableString.range(of: model.detail))
return fullString
}
Any thoughts on how to achieve the image on the right?
Question Update 1
The code below is working! There is only one slight problem. When I add lineSpacing, there is extra space at the end of the last line in main string. Notice that I have this set to zero: mainStringParagraphStyle.paragraphSpacing = 0, but there is still space at the end because mainStringParagraphStyle.lineSpacing = 60.
The reason I ask this is to have more fine grain control of spacing. For example, have a perfect number between the line image and main string. Any thoughts on this?
I put code and picture below:
Code:
func createAttributedString(for model: BookModel) -> NSMutableAttributedString {
let fullString = NSMutableAttributedString()
let mainStringParagraphStyle = NSMutableParagraphStyle()
mainStringParagraphStyle.alignment = .center
mainStringParagraphStyle.paragraphSpacing = 0 // The space after the end of the paragraph
mainStringParagraphStyle.lineSpacing = 60 // NOTE: This controls the spacing after the last line instead of just `paragraphSpacing`
let mainString = NSAttributedString(string: "\(model.main)\n",
attributes: [.paragraphStyle: mainStringParagraphStyle, .font: UIFont.systemFont(ofSize: 24)])
let lineImageStringParagraphStyle = NSMutableParagraphStyle()
lineImageStringParagraphStyle.alignment = .center
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-view"))
let lineImageString = NSMutableAttributedString(attachment: lineImageAttachment)
lineImageString.addAttribute(.paragraphStyle, value: lineImageStringParagraphStyle, range: NSRange(location: 0, length: lineImageString.length))
let detailStringParagraphStyle = NSMutableParagraphStyle()
detailStringParagraphStyle.alignment = .center
detailStringParagraphStyle.paragraphSpacingBefore = 5 // The distance between the paragraph’s top and the beginning of its text content
detailStringParagraphStyle.lineSpacing = 0
let detailString = NSAttributedString(string: "\n\(model.detail)",
attributes: [.paragraphStyle: detailStringParagraphStyle, .font: UIFont.systemFont(ofSize: 12)])
fullString.append(mainString)
fullString.append(lineImageString)
fullString.append(detailString)
return fullString
}
Updated answer:
Here's a new example. I set the spacing at the top and at the bottom of the paragraph with the image. This allows line breaks to be used in model.main and model.detail if needed. Also, instead of lineSpacing, I used lineHeightMultiple. This parameter affects the indentation between lines without affecting the last line:
func createAttributedString(for model: BookModel) -> NSAttributedString {
let fullString = NSMutableAttributedString()
let mainStringParagraphStyle = NSMutableParagraphStyle()
mainStringParagraphStyle.alignment = .center
mainStringParagraphStyle.lineHeightMultiple = 2 // Note that this is a multiplier, not a value in points
let mainString = NSAttributedString(string: "\(model.main)\n", attributes: [.paragraphStyle: mainStringParagraphStyle, .font: UIFont.systemFont(ofSize: 24)])
let lineImageStringParagraphStyle = NSMutableParagraphStyle()
lineImageStringParagraphStyle.alignment = .center
lineImageStringParagraphStyle.paragraphSpacingBefore = 10 // The space before image
lineImageStringParagraphStyle.paragraphSpacing = 20 // The space after image
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-image"))
let lineImageString = NSMutableAttributedString(attachment: lineImageAttachment)
lineImageString.addAttribute(.paragraphStyle, value: lineImageStringParagraphStyle, range: NSRange(location: 0, length: lineImageString.length))
let detailStringParagraphStyle = NSMutableParagraphStyle()
detailStringParagraphStyle.alignment = .center
let detailString = NSAttributedString(string: "\n\(model.detail)", attributes: [.paragraphStyle: detailStringParagraphStyle, .font: UIFont.systemFont(ofSize: 12)])
fullString.append(mainString)
fullString.append(lineImageString)
fullString.append(detailString)
return fullString
}
Also have a look at my library StringEx. It allows you to create a NSAttributedString from the template and apply styles without having to write a ton of code:
import StringEx
...
func createAttributedString(for model: BookModel) -> NSAttributedString {
let pattern = "<main />\n<image />\n<detail />"
let ex = pattern.ex
ex[.tag("main")]
.insert(model.main)
.style([
.aligment(.center),
.lineHeightMultiple(2),
.font(.systemFont(ofSize: 24))
])
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-image"))
let lineImageString = NSAttributedString(attachment: lineImageAttachment)
ex[.tag("image")]
.insert(lineImageString)
.style([
.aligment(.center),
.paragraphSpacingBefore(10),
.paragraphSpacing(20)
])
ex[.tag("detail")]
.insert(model.detail)
.style([
.aligment(.center),
.font(.systemFont(ofSize: 12))
])
return ex.attributedString
}
Old answer:
I think you can just set the spacing at the end of the first paragraph (main string) and the spacing at the beginning of the last paragraph (detail string):
func createAttributedString(for model: BookModel) -> NSMutableAttributedString {
let fullString = NSMutableAttributedString()
let mainStringParagraphStyle = NSMutableParagraphStyle()
mainStringParagraphStyle.alignment = .center
mainStringParagraphStyle.paragraphSpacing = 30 // The space after the end of the paragraph
let mainString = NSAttributedString(string: "\(model.main)\n", attributes: [.paragraphStyle: mainStringParagraphStyle])
let lineImageStringParagraphStyle = NSMutableParagraphStyle()
lineImageStringParagraphStyle.alignment = .center
let lineImageAttachment = NSTextAttachment(image: #imageLiteral(resourceName: "line-image"))
let lineImageString = NSMutableAttributedString(attachment: lineImageAttachment)
lineImageString.addAttribute(.paragraphStyle, value: lineImageStringParagraphStyle, range: NSRange(location: 0, length: lineImageString.length))
let detailStringParagraphStyle = NSMutableParagraphStyle()
detailStringParagraphStyle.alignment = .center
detailStringParagraphStyle.paragraphSpacingBefore = 5 // The distance between the paragraph’s top and the beginning of its text content
let detailString = NSAttributedString(string: "\n\(model.detail)", attributes: [.paragraphStyle: detailStringParagraphStyle])
fullString.append(mainString)
fullString.append(lineImageString)
fullString.append(detailString)
return fullString
}
Related
I am looking for align Text in NSAttributedString. It should be on same indent(List of string on same Column).
Output:
I have used below code
func setAttributedText() {
let image1Attachment = NSTextAttachment()
image1Attachment.image = UIImage(named: "checkgreen.png")
image1Attachment.bounds = CGRect(x: 0,
y: (contentLabel.font.capHeight - image1Attachment.image!.size.height).rounded() / 2, width: image1Attachment.image!.size.width,
height: image1Attachment.image!.size.height)
var attributes = [NSAttributedString.Key: Any]()
attributes[.font] = UIFont.preferredFont(forTextStyle: .body)
attributes[.foregroundColor] = UIColor.black
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 50
attributes[.paragraphStyle] = paragraphStyle
let line0 = NSAttributedString(string: "Dummy text is text that is used in the publishing industry or by web designers to occupy the space which will later be filled with 'real' content. \n")
let line1 = NSAttributedString(string: "Your account will be charged for renewal within 24-hours prior to the end of the current subscription perio\n")
// let line2 = NSAttributedString(string: "You can manage your subscriptions and turn off auto-renewal by going to your Account Settings on th")
let checkgreenImageAttribute = NSMutableAttributedString(attributedString: NSAttributedString(attachment: image1Attachment))
let finalString = NSMutableAttributedString(attributedString: checkgreenImageAttribute)
finalString.append(line0)
finalString.append(checkgreenImageAttribute)
finalString.append(line1)
// finalString.append(checkgreenImageAttribute)
// finalString.append(line2)
contentLabel.attributedText = finalString
}
NOTE: i don't want to use bullet points
In an attributed string, a NSTextAttachment becomes a character in the string.
So, apply your attributes to the entire string after you've "assembled" it:
let checkgreenImageAttribute = NSMutableAttributedString(attributedString: NSAttributedString(attachment: image1Attachment))
let finalString = NSMutableAttributedString(attributedString: checkgreenImageAttribute)
finalString.append(line0)
finalString.append(checkgreenImageAttribute)
finalString.append(line1)
// apply paragraph attributes here
finalString.addAttributes(attributes, range: NSRange(location: 0, length: finalString.length))
I want to remove the bottom padding of a UILabel with attributedText inside a UIStackview.
I found this solution How to remove the extra padding below an one line UILabel. This works with normal text but not with attributed text.
let textLabel = UILabel()
textLabel.translatesAutoresizingMaskIntoConstraints = false
textLabel.text = "What is a chemical property and how can you observe it?"
textLabel.numberOfLines = 0
textLabel.lineBreakMode = .byWordWrapping
textLabel.backgroundColor = .lightGray
mainStackView.addArrangedSubview(textLabel)
let textLabel2 = UILabel()
textLabel2.translatesAutoresizingMaskIntoConstraints = false
let html = "<html lang=\"en\"><head><meta charset=\"UTF-8\"></head><body><div style=\"font-size:36;\"><p>What is a <em>chemical property</em> and how can you observe it?</p></div></body></html>"
let data = Data(html.utf8)
if let attributedString = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {
let a = NSMutableAttributedString.init(attributedString: attributedString)
let range = (a.string as NSString).range(of: a.string)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
paragraphStyle.firstLineHeadIndent = 0.0
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.black,
.paragraphStyle: paragraphStyle
]
a.addAttributes(attributes, range: range)
textLabel2.attributedText = a
}
textLabel2.numberOfLines = 0
textLabel2.lineBreakMode = .byWordWrapping
textLabel2.backgroundColor = .yellow
mainStackView.addArrangedSubview(textLabel2)
let textLabel3 = UILabel()
textLabel3.translatesAutoresizingMaskIntoConstraints = false
textLabel3.text = "What is a chemical property and how can you observe it?"
textLabel3.numberOfLines = 0
textLabel3.lineBreakMode = .byWordWrapping
textLabel3.backgroundColor = .lightGray
mainStackView.addArrangedSubview(textLabel3)
A working sample project with this code can be found here: https://github.com/Quobject/testUIlabelInStackviewpadding
The "bottom spacing" is not "spacing" ... your converted <p>...</p> html block adds a newline character at the end of the text.
You can use this extension (found here):
extension NSMutableAttributedString {
func trimmedAttributedString() -> NSAttributedString {
let invertedSet = CharacterSet.whitespacesAndNewlines.inverted
let startRange = string.rangeOfCharacter(from: invertedSet)
let endRange = string.rangeOfCharacter(from: invertedSet, options: .backwards)
guard let startLocation = startRange?.upperBound, let endLocation = endRange?.lowerBound else {
return NSAttributedString(string: string)
}
let location = string.distance(from: string.startIndex, to: startLocation) - 1
let length = string.distance(from: startLocation, to: endLocation) + 2
let range = NSRange(location: location, length: length)
return attributedSubstring(from: range)
}
}
and change this line:
textLabel2.attributedText = a
to:
textLabel2.attributedText = a.trimmedAttributedString()
Result (applying that change to your GitHub repo):
I have a label with text and want to add more test in running time, but I want to add different style to this text that was add. Is there a way to do this?
This is the code
label.text = (label.text ?? "") + " \n \(userName)"
How do I add style to userName without changing the style of the label?
use attributed text in UILabel:
here some code.
a) some useful typedefs:
typealias AttrDict = [NSAttributedString.Key : Any]
a) create some styles:
func textAttribute() -> AttrDict{
let textFont = UIFont.systemFont(ofSize: 32)
let stdAttrib = [NSAttributedString.Key.font: textFont,
//NSParagraphStyleAttributeName: paragraphStyle2,
NSAttributedString.Key.foregroundColor: UIColor.red] as AttrDict
return stdAttrib
}
func smallTextAttribute() -> AttrDict{
let textFont = UIFont.systemFont(ofSize: 10)
let stdAttrib = [NSAttributedString.Key.font: textFont,
NSAttributedString.Key.foregroundColor: UIColor.green] as AttrDict
return stdAttrib
}
c) build Your attributed String:
func myAttributedString() -> NSAttributedString {
let pieces = ["hello", "word"]
let resultAttributed = NSMutableAttributedString()
var s = ""
s = pieces[0] + "\n"
resultAttributed.append(NSAttributedString(string: s,
attributes: stdTextAttribute() ))
s = pieces[1] + "\n"
resultAttributed.append(NSAttributedString(string: s,
attributes: smallTextAttribute() ))
return resultAttributed
}
d) put in Your label/textView:
....
class ViewController: UIViewController {
#IBOutlet weak var myLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
myLabel.numberOfLines = 0
myLabel.attributedText = myAttributedString()
}
}
I made a GIST:
https://gist.github.com/ingconti/aefc78c6d0b22f5329f906094c312a21
PLS connect UIlabel..
:)
To do this, you need to use NSMutableAttributedString, NSAttributedString and use label.attributedText, How?
Doing this:
let userName = "StackOverflow"
let prefixText = NSAttributedString(string: "this is normal ")
let font = UIFont.systemFont(ofSize: 40)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: UIColor.blue
]
let userNameWithStyle = NSAttributedString(string: userName, attributes: attributes)
let finalString = NSMutableAttributedString(attributedString: prefixText)
finalString.append(userNameWithStyle)
self.label.attributedText = finalString
Image result
I need to add bulleted text to textView in iOS app. I am looking at this link and this one and following their ideas. This is my code:
let paragraph = NSMutableParagraphStyle()
paragraph.firstLineHeadIndent = 15
paragraph.headIndent = 15
attributes = [
NSAttributedStringKey.paragraphStyle: paragraph
]
attributedString = NSAttributedString(string: "\u{2022} Some text some text some text some text some text some text", attributes: attributes)
finalText.append(attributedString)
What I need is to get the text indented with the start of the text above. Like it is in the picture:
What I get is the text indented with the starting point of the bullet.
Remove paragraph.firstLineHeadIndent = 15 from code...
let paragraph = NSMutableParagraphStyle()
paragraph.headIndent = 15
attributes = [
NSAttributedStringKey.paragraphStyle: paragraph
]
attributedString = NSAttributedString(string: "\u{2022} Some text some text some text some text some text some text", attributes: attributes)
finalText.append(attributedString)
Please refer my sample code and screenshot
let style = NSMutableParagraphStyle()
style.alignment = .left
style.headIndent = 20
let title = NSMutableAttributedString(string: "\u{2022} I need to add bulleted text to textView in iOS app. I am looking at this link and this one and following their ideas. This is my code:", attributes: [NSAttributedStringKey.paragraphStyle: style,NSAttributedStringKey.foregroundColor:UIColor.blue])
let titleStr = NSMutableAttributedString(string: "\n\n\u{2022} I need to add bulleted text to textView in iOS app. I am looking at this link and this one and following their ideas. This is my code:", attributes: [NSAttributedStringKey.paragraphStyle: style,NSAttributedStringKey.foregroundColor:UIColor.blue])
title.append(titleStr)
titleLabel.attributedText = title
I faced same problem with textView i used custom indent & it working fine-
#IBOutlet var bulletTextView: UITextView!
override func viewDidLoad() {
let bullet1 = "This is a small string,This is a small string,This is a small string,This is a small string,This is a small string,This is a small string,This is a small string"
let bullet2 = "This is more of medium string with a few more words etc."
let bullet3 = "Well this is certainly a longer string, with many more words than either of the previuos two strings"
strings = [bullet1, bullet2, bullet3]
let fullAttributedString = NSMutableAttributedString()
for string: String in strings {
let attributesDictionary:[NSAttributedStringKey:Any] = [NSAttributedStringKey.font : bulletTextView.font,NSAttributedStringKey.foregroundColor : UIColor.red]
let bulletPoint: String = "\u{2022}"
//let formattedString: String = "\(bulletPoint) \(string)\n"
let attributedString = NSMutableAttributedString(string: bulletPoint, attributes: attributesDictionary)
attributedString.append(NSAttributedString(string: " \(string) \n"))
let indent:CGFloat = 15
let paragraphStyle = createParagraphAttribute(tabStopLocation: indent, defaultTabInterval: indent, firstLineHeadIndent: indent - 10, headIndent: indent)
attributedString.addAttributes([NSAttributedStringKey.paragraphStyle: paragraphStyle], range: NSMakeRange(0, attributedString.length))
fullAttributedString.append(attributedString)
}
bulletTextView.attributedText = fullAttributedString
}
func createParagraphAttribute(tabStopLocation:CGFloat, defaultTabInterval:CGFloat, firstLineHeadIndent:CGFloat, headIndent:CGFloat) -> NSParagraphStyle {
let paragraphStyle: NSMutableParagraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let options:[NSTextTab.OptionKey:Any] = [:]
paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: tabStopLocation, options: options)]
paragraphStyle.defaultTabInterval = defaultTabInterval
paragraphStyle.firstLineHeadIndent = firstLineHeadIndent
paragraphStyle.headIndent = headIndent
return paragraphStyle
}
Output:-
Set paragraph.firstLineHeadIndent to zero. This indents only lines starting with the second one. Currently, you are indenting all lines…
let paragraph = NSMutableParagraphStyle()
// paragraph.firstLineHeadIndent = 15
paragraph.headIndent = 15
To have the headIndent resize with dynamic fonts I'm using this:
private func updateUI() {
let bullet: NSString = "• "
var attributes = [NSAttributedString.Key: Any]()
let paragraph = NSMutableParagraphStyle()
leStackView.subviews.compactMap({ $0 as? UILabel }).forEach {
attributes[.font] = $0.font
paragraph.headIndent = bullet.size(withAttributes: attributes).width
attributes[.paragraphStyle] = paragraph
let text = $0.text ?? ""
$0.attributedText = NSAttributedString(string: text, attributes: attributes)
}
}
The labels for each bullet point are set up in the storyboard with plain text (including the bullet) and dynamic fonts.
I really appreciate the contributions to this thread plus https://bendodson.com/weblog/2018/08/09/bulleted-lists-with-uilabel/
Simple solution:
extension Sequence where Self.Element == String {
func toBulletList(_ bulletIndicator: String = "•",
itemSeparator: String = "\n",
spaceCount: Int = 2) -> String {
let bullet = bulletIndicator + String(repeating: " ", count: spaceCount)
let list = self
.map { bullet + $0 }
.reduce("", { $0 + ($0.isEmpty ? $0 : itemSeparator) + $1 })
return list
}
}
usage:
let items: [String] = [
"one",
"two",
"three"
]
let list = items.toBulletList()
po list ->
• one
• two
• three
I had the same problem, and I finally realized that Label didn't support it. If you want to use bullet-list in the same rows you should use text view
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.
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)
}
}
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
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.
One of the possible ways to make it work with the function mentioned in the question - to modify stringList parameter.
First of all let's define model class BulletString:
class BulletString {
var text: String
var attributes: [NSAttributedStringKey : Any]?
init(string: String) {
text = string
}
}
Now your bullet stringList in you function should be [BulletString] type. Define two bulletStrings and pass them to your function. Here is a working solution with your function:
let bulletString1 = BulletString.init(string: "string1")
bulletString1.attributes = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18.0)]
let bulletString2 = BulletString.init(string: "string2")
let bullets = [bulletString1, bulletString2]
label.attributedText = bulletPoints(stringList: bullets, font: UIFont.systemFont(ofSize: 17), bullet: "•", lineSpacing: 4, paragraphSpacing: 4, textColor: UIColor.darkGray, bulletColor: UIColor.darkGray)
label.textColor = .black
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 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 bulletString in stringList {
let formattedString = "\(bullet)\t\(bulletString.text)\n"
let attributedString = NSMutableAttributedString(string: formattedString)
attributedString.addAttributes(
[NSAttributedStringKey.paragraphStyle : paragraphStyle],
range: NSMakeRange(0, attributedString.length))
attributedString.addAttributes(
textAttributes,
range: NSMakeRange(0, attributedString.length))
// Here your custom attributes you provided in BulletString
if let attr = bulletString.attributes {
attributedString.addAttributes(attr, 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
}
Results