Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
504 views
in Technique[技术] by (71.8m points)

ios - NSAttributedString with padding before and after the text and a background

I want to style a text using NSAttributedString. The text should have a background and a custom padding, so that the text has a little bit of space to the background's edge.

This is what I want to achieve:

what I want to achieve

This is what I don't want to achieve (background in the second line is not word / character-specific):

what I don't want to achieve

And this is the code that I tried in a playground:

let quote = "some text with a lot of other text and 
some other text."
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .left
let attributes: [NSAttributedString.Key: Any] = [
    NSAttributedString.Key.paragraphStyle: paragraphStyle,
    NSAttributedString.Key.backgroundColor: UIColor.red,
    NSAttributedString.Key.foregroundColor: UIColor.white
]
let attributedQuote = NSAttributedString(string: quote, attributes: attributes)

And this is what the playground preview renders:

[![playground preview][3]][3]

The text is very close to the edge of the background. Is there any way to get the background of the text to have some space to the text? I need some padding. I tried using headIndent but that would move the text with it's background to the right, not just the text. Therefor it is not useful for padding.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The text should have a background and a custom padding, so that the text has a little bit of space to the background's edge.

The best way I found is using TextKit, it's a little bit cumbersome but it's completely modular and is made for this purpose.
In my view, it isn't up to the TextView itself to draw the rectangles in its draw method, that's the LayoutManager's work.

The entire classes used in the project are provided hereafter in order to ease the work with copy-paste (Swift 5.1 - iOS 13).

AppDelegate.swift stores the property to get the text wherever you are in the app.

class AppDelegate: UIResponder, UIApplicationDelegate {

    lazy var TextToBeRead: NSAttributedString = {

        var text: String
        if let filepath = Bundle.main.path(forResource: "TextToBeRead", ofType: "txt") {
            do { text = try String(contentsOfFile: filepath) }
            catch { text = "E.R.R.O.R." }
        } else { text = "N.O.T.H.I.N.G." }

        return NSAttributedString(string: text)
    }()
}

ViewController.swift ? only one single text view at full screen.

class ViewController: UIViewController, NSLayoutManagerDelegate {

    @IBOutlet weak var myTextView: UITextView!
    let textStorage = MyTextStorage()
    let layoutManager = MyLayoutManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.layoutManager.delegate = self

        self.textStorage.addLayoutManager(self.layoutManager)
        self.layoutManager.addTextContainer(myTextView.textContainer)

        let appDelegate = UIApplication.shared.delegate as? AppDelegate
        self.textStorage.replaceCharacters(in: NSRange(location: 0, length: 0),
                                           with: (appDelegate?.TextToBeRead.string)!)
    }

    func layoutManager(_ layoutManager: NSLayoutManager,
                       lineSpacingAfterGlyphAt glyphIndex: Int,
                       withProposedLineFragmentRect rect: CGRect) -> CGFloat { return 20.0 }

    func layoutManager(_ layoutManager: NSLayoutManager,
                       paragraphSpacingAfterGlyphAt glyphIndex: Int,
                       withProposedLineFragmentRect rect: CGRect) -> CGFloat { return 30.0 }
}

MyTextStorage.swift

class MyTextStorage: NSTextStorage {

    var backingStorage: NSMutableAttributedString

    override init() {

        backingStorage = NSMutableAttributedString()
        super.init()
    }

    required init?(coder: NSCoder) {

        backingStorage = NSMutableAttributedString()
        super.init(coder: coder)
    }

//    Overriden GETTERS
    override var string: String {
        get { return self.backingStorage.string }
    }

    override func attributes(at location: Int,
                             effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {

        return backingStorage.attributes(at: location, effectiveRange: range)
    }

//    Overriden SETTERS
    override func replaceCharacters(in range: NSRange, with str: String) {

        backingStorage.replaceCharacters(in: range, with: str)
        self.edited(.editedCharacters,
                    range: range,
                    changeInLength: str.count - range.length)
    }

    override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {

        backingStorage.setAttributes(attrs, range: range)
        self.edited(.editedAttributes,
                    range: range,
                    changeInLength: 0)
    }
}

MyLayoutManager.swift

import CoreGraphics //Important to draw the rectangles

class MyLayoutManager: NSLayoutManager {

    override init() { super.init() }

    required init?(coder: NSCoder) { super.init(coder: coder) }

    override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {   
        super.drawBackground(forGlyphRange: glyphsToShow, at: origin)

        self.enumerateLineFragments(forGlyphRange: glyphsToShow) { (rect, usedRect, textContainer, glyphRange, stop) in

            var lineRect = usedRect
            lineRect.size.height = 30.0

            let currentContext = UIGraphicsGetCurrentContext()
            currentContext?.saveGState()

            currentContext?.setStrokeColor(UIColor.red.cgColor)
            currentContext?.setLineWidth(1.0)
            currentContext?.stroke(lineRect)

            currentContext?.restoreGState()
        }
    }
}

... and here's what it looks like in the end: enter image description here

There's only to customize the colors and adjust few parameters to stick to your project but this is the rationale to display a NSAttributedString with padding before and after the text and a background... for much more than 2 lines if need be.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...