0

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:

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:

test

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
    }
JEL
  • 1,540
  • 4
  • 23
  • 51

1 Answers1

2

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
}

enter image description here

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
}

Results

andruvs
  • 79
  • 1
  • 3
  • Hi @andruvs - I tried your example and the same issue happens. The `detailStringParagraphStyle.paragraphSpacingBefore` does not work and the spacing is equal to `mainStringParagraphStyle.paragraphSpacing`. Also, one thing I noticed you took out was `lineSpacing` but I would like this so I can control spacing between lines as well. Any thoughts on how to fix all this? Thank you – JEL Jan 27 '21 at 23:20
  • Hi @JEL Have you used my example exactly? Pay attention, I made `fullString` of two paragraphs and a picture between them. The result of execution with `model.main = "Hello this is the string called main"` and `model.detail = "Detail string"` is shown in the screenshot. Maybe the `model.detail` inside itself contains line breaks at the beginning of the string? Then in this case additional indentation will appear. Also you can freely add `lineSpacing` parameter to the paragraph styles, I removed this parameter because it does not address the indentation problem. – andruvs Jan 28 '21 at 08:34
  • Hi @andruvs! What you have works! I tested in sample app - for some reason my app didn't. I just have one small update to the question if you could see above. Its a small issue, but let me know please! – JEL Jan 29 '21 at 21:50
  • @JEL, I have updated the answer. Take a look at a new example. – andruvs Jan 30 '21 at 10:26
  • hey @andruvs, this is amazing! Works great! Thank you for taking the time to update answer. Your library looks great, but won't use it for this current use case. Glad to know there is something out there. Thank you for helping! – JEL Jan 30 '21 at 17:44
  • Hi there @andruvs! I have a related question to this for auto shrinking the text in this code. It messes up the paragraph spacing. I was hoping you could help in anyway that would be awesome. If you have time, please see this link: https://stackoverflow.com/questions/68050069/auto-shrink-label-messes-up-paragraph-spacing – JEL Jun 19 '21 at 19:26
  • Hey @andruvs, I put a bounty on the new question. Could you please check it out, would really appreciate the help! Link: https://stackoverflow.com/questions/68050069/auto-shrink-label-messes-up-paragraph-spacing – JEL Jun 21 '21 at 21:09