UITextView
does not support any kind of tables, but I'm trying to create a text view implementation which would be able to show tables alongside normal text.
Basically: My macOS port uses NSTextTable
to render some paragraphs next to each other when creating a PDF. I'm trying to come up with a way to achieve this on iOS.
Documentation and real-world implementations of TextKit 2 are still a bit hazy, but I'm interested if it would be possible to create my own content types using NSTextElement
and NSTextElementProvider
. If I'm reading it correctly, I'd define a range which is governed by my own implementation/layout?
As the documentation is so sparse, I'm very unsure how to actually implement an NSTextElement
which would also be rendered as text when saved as a PDF — or if any of this would even be possible.
Any help or hints would be appreciated.
There is no real way around the actual problem, but you can create a view provider which sort of acts like a table.
This example just supports one row, so it's basically just a column view, but you should be able to expand it pretty easily. Sizing the cells will be the trickiest part if you want to have a two-dimensional table.
The actual text attachment, which registers a view provider to display the fake table:
@objc public class TableAttachment:NSTextAttachment {
// We need to forward these values to the provider when needed
var cells:[TextTableCell]?
var spacing = 0.0
var margin = 0.0
@objc public init(cells:[TextTableCell], spacing:CGFloat = 0.0, margin:CGFloat = 0.0) {
NSTextAttachment.registerViewProviderClass(TextTableProvider.self, forFileType: "public.data")
self.cells = cells
self.spacing = spacing
self.margin = margin
super.init(data: nil, ofType: "public.data")
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
A label which acts as a table cell:
@objc public class TextTableCell:UILabel {
var width = 0.0
@objc public init(content:NSAttributedString, width: Double, height:Double = 0.0) {
self.width = width
let rect = CGRect(x: 0.0, y: 0.0, width: width, height: height)
super.init(frame: rect)
self.attributedText = content
self.lineBreakMode = .byWordWrapping
self.numberOfLines = 50 // Arbitrary value
// Make the text field behave correctly in a PDF
self.layer.shouldRasterize = false
}
var height:CGFloat {
guard var attributedText = self.attributedText else { return 0.0 }
// Calculate frame size
let rect = attributedText.boundingRect(with: CGSize(width: self.frame.width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
self.frame.size.height = rect.size.height
self.frame.origin.y = 0
return self.frame.height
}
}
And finally, the text attachment provider, which takes in a bunch of table cells and displays them.
@objc public class TextTableProvider : NSTextAttachmentViewProvider {
var cells:[TextTableCell] = []
var spacing = 0.0
var margin = 0.0
override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) {
if let attachment = textAttachment as? TableAttachment {
self.cells = attachment.cells ?? []
self.spacing = attachment.spacing
self.margin = attachment.margin
}
super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location)
tracksTextAttachmentViewBounds = true
}
@objc public override func loadView() {
super.loadView()
view = UIView()
// Add subviews
var x = self.margin
for column in self.cells {
column.frame.origin.x = x
view?.addSubview(column)
x = spacing + CGRectGetMaxX(column.frame)
}
}
/// Returns height of the tallest column in this view
@objc public override func attachmentBounds(
for attributes: [NSAttributedString.Key : Any],
location: NSTextLocation,
textContainer: NSTextContainer?,
proposedLineFragment: CGRect,
position: CGPoint
) -> CGRect {
var height = 0.0
for column in self.cells {
let colHeight = column.height
if colHeight > height { height = colHeight }
}
// I have no idea what this - 15.0 is, something for my own purposes, probably
return CGRect(x: 0, y: 0, width: proposedLineFragment.width - 15.0, height: height)
}
}