NSAttributedString带有文本前后填充和背景。

9
我想使用NSAttributedString来对文本进行样式设置。该文本应具有背景和自定义填充,以使文本与背景的边缘之间留有一点空间。 这就是我想要实现的效果: what I want to achieve 这不是我想要实现的效果(第二行的背景不是特定的单词/字符): what I don't want to achieve 这是我在Playground中尝试的代码:
let quote = "some text with a lot of other text and \nsome 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)

以下是游乐场预览的渲染效果:

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

文本非常靠近背景边缘。有没有办法让文本的背景与文本之间留出一些空间?我需要一些衬距。

我尝试使用headIndent,但那会将文本与其背景一起向右移动,而不仅仅是文本。因此,它对于衬距并不有用。


@Starsky,标签的问题在于背景始终与标签的宽度一样宽。但我希望背景仅在字符后面可见。这样每行都有一个背景,只延伸到字符所在的位置。(请参见更新的问题以查看图像) - WalterBeiter
如果在每行后面添加 \n 并将其变为白色呢? - Alexandr Kolesnik
你有没有考虑使用UIStackView?这将允许您为每个UILabel提供自己的样式,并使用stackview的间距属性进行间距控制。只是一个想法? - David Thorn
好的,这是一个有趣的想法,不过那我就得自己处理换行了。 - WalterBeiter
1
@WalterBeiter:看了一下https://dev59.com/4YXca4cB1Zd3GeqPOd3j#28042708,这可能是你问题的好解决方案吗? - XLE_22
显示剩余7条评论
2个回答

8

文本应该有一个背景和自定义填充,以便文本与背景边缘之间留有一定空间。

我发现最好的方法是使用TextKit,虽然它可能有点繁琐,但完全模块化并且是为此目的而设计的。
在我看来,TextView本身不应该在其draw方法中绘制矩形,这是LayoutManager的工作。

下面提供了项目中使用的整个类,以便于复制粘贴(Swift 5.1 - iOS 13)。

AppDelegate.swift存储了在应用程序中任何位置获取文本的属性。

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 ⟹ 只有一个全屏幕的文本视图。

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()
        }
    }
}

以下是最终的效果:

enter image description here

只需自定义颜色和调整一些参数,就可以适应您的项目,但这是显示带有文本前后填充和背景的NSAttributedString的基本原理…如果需要的话,可用于超过2行以上的文本。


0
使用TextKit 2:
创建一个UITextView并将textLayoutManager的委托设置为它。
let textView = UITextView(usingTextLayoutManager: true)
textView.textLayoutManager?.delegate = self

实现委托以返回自定义的NSTextLayoutFragment。
func textLayoutManager(
    _ textLayoutManager: NSTextLayoutManager,
    textLayoutFragmentFor location: NSTextLocation,
    in textElement: NSTextElement) -> NSTextLayoutFragment {

    PaddingLayoutFragment(
        textElement: textElement,
        range: textElement.elementRange)
}

自定义NSTextLayoutFragment的实现。这是我们在背景上绘制一些额外填充的地方。
class SubLayoutFragment: NSTextLayoutFragment {
    
    override func draw(at point: CGPoint, in context: CGContext) {
        for lineFragment in textLineFragments {
            let rect = lineFragment.typographicBounds
                .insetBy(dx: -20, dy: 0) // hardcoded padding
            context.saveGState()
            context.setFillColor(UIColor.red.cgColor)
            context.fill(rect)
            context.restoreGState()
        }
        super.draw(at: point, in: context)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接