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:
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.