I would like to print a NSTextStorage
on multiple pages and add annotations to the side margins corresponding to certain text ranges. For example, for all occurrences of # at the start of a line, the side margin should show an automatically increasing number.
My idea was to create a NSLayoutManager
and dynamically add NSTextContainer
instances to it until all text is laid out. The layoutManager would then allow me to get the bounding rectangle of the interesting text ranges so that I can draw the corresponding numbers at the same height inside the side margin. This approach works well on macOS, but I'm having some issues on iOS.
When running the code below in an iPad Simulator, I would expect that the print preview shows 3 pages, the first with the numbers 0-1, the second with the numbers 2-3, and the last one with the number 4. Instead the first page shows the number 4, the second one the numbers 2-4, and the last one the numbers 0-4. It's as if the pages are inverted, and each page shows the text starting at the correct location but always ending at the end of the complete text (and not the range assigned to the relative textContainer).
I've created FB17026419.
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
let printController = UIPrintInteractionController.shared
let printPageRenderer = PrintPageRenderer()
printPageRenderer.pageSize = CGSize(width: 100, height: 100)
printPageRenderer.textStorage = NSTextStorage(string: (0..<5).map({ "\($0)" }).joined(separator: "\n"), attributes: [.font: UIFont.systemFont(ofSize: 30)])
printController.printPageRenderer = printPageRenderer
printController.present(animated: true) { _, _, error in
if let error = error {
print(error.localizedDescription)
}
}
}
}
class PrintPageRenderer: UIPrintPageRenderer, NSLayoutManagerDelegate {
var pageSize: CGSize!
var textStorage: NSTextStorage!
private let layoutManager = NSLayoutManager()
private var textViews = [UITextView]()
override var numberOfPages: Int {
if !Thread.isMainThread {
return DispatchQueue.main.sync { [self] in
numberOfPages
}
}
printFormatters = nil
layoutManager.delegate = self
textStorage.addLayoutManager(layoutManager)
if textStorage.length > 0 {
let glyphRange = layoutManager.glyphRange(forCharacterRange: NSRange(location: textStorage.length - 1, length: 0), actualCharacterRange: nil)
layoutManager.textContainer(forGlyphAt: glyphRange.location, effectiveRange: nil)
}
var page = 0
for textView in textViews {
let printFormatter = textView.viewPrintFormatter()
addPrintFormatter(printFormatter, startingAtPageAt: page)
page += printFormatter.pageCount
}
return page
}
func layoutManager(_ layoutManager: NSLayoutManager, didCompleteLayoutFor textContainer: NSTextContainer?, atEnd layoutFinishedFlag: Bool) {
if textContainer == nil {
addPage()
}
}
private func addPage() {
let textContainer = NSTextContainer(size: pageSize)
layoutManager.addTextContainer(textContainer)
let textView = UITextView(frame: CGRect(origin: .zero, size: pageSize), textContainer: textContainer)
textViews.append(textView)
}
}
Printing isn't my domain, but from the TextKit perspetive, the pages are inverted because creating a new UITextView
with a text container triggers a new layout process. If you add print
in the following way:
private func addPage() {
let textContainer = NSTextContainer(size: pageSize)
print("Appending text container...")
layoutManager.addTextContainer(textContainer)
let textView = UITextView(frame: CGRect(origin: .zero, size: pageSize), textContainer: textContainer)
print("Appending text view...")
textViews.append(textView)
}
You will see the following log when running your app:
Appending text container...
Appending text container...
Appending text container...
Appending text view...
Appending text view...
Appending text view...
To fix the issue, consider creating the text views after all the text containers are added, as shown below:
override var numberOfPages: Int {
...
if textStorage.length > 0 {
...
}
for textContainer in layoutManager.textContainers {
let textView = UITextView(frame: CGRect(origin: .zero, size: textContainer.size), textContainer: textContainer)
textViews.append(textView)
}
for textView in textViews {
let range = layoutManager.glyphRange(for: textView.textContainer)
print("textrange = \(range)")
}
var page = 0
...
}
...
private func addPage() {
let textContainer = NSTextContainer(size: pageSize)
layoutManager.addTextContainer(textContainer)
//let textView = UITextView(frame: CGRect(origin: .zero, size: pageSize), textContainer: textContainer)
//textViews.append(textView)
}
With that, running your app gets the following text ranges, which I believe is what you are looking for:
textrange = {0, 4}
textrange = {4, 4}
textrange = {8, 1}
Best,
——
Ziqiao Chen
Worldwide Developer Relations.