This issue has already been reported to Apple via the Feedback Assistant as FB12401598 and a code-level support incident has been opened to follow up, but so far I haven't heard anything.
The problem is that the NSAttributedString used to be able to load HTML files correctly via the [NSAttributedString initWithHTML:options:documentAttributes:] method (or its Swift equivalent). As of the developer beta of Sonoma, however, this no longer works. The method loads the attributedString but HTML tables are completely ignored. Every cell in the table just appears on a new line.
The app I'm working on has a bunch of HTML templates that get drawn inside another View using an NSAttributedString. This has worked for years but no longer works on Sonoma.
Does anyone know a decent workaround for correctly drawing some formatted text whose formatting is specified via HTML? I'm currently exploring the idea of converting the HTML files to RTF on an older system and using RTF, but the RTF format isn't nearly as simple as HTML.
Here's a screenshot of a simple project and Safari showing the same HTML side by side
I have developed a workaround. Basically, you can copy each cell, using the first table object found for a given set of table cells. It's ugly, but does seem to work. Including some code below to show how you can do it.
class ViewController: NSViewController {
@IBOutlet var textView: NSTextView!
override func viewDidAppear() {
super.viewDidAppear()
let html =
"""
<html>
<head>
<title>Testing Tables</title>
<style>
td {
border: 1px solid;
padding: 10px;
}
</style>
</head>
<body>
<table>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
<tr>
<td>Cell 3</td>
<td>Cell 4</td>
</tr>
</table>
</body>
</html>
"""
let data = html.data(using: .utf8)!
let string = NSMutableAttributedString(html: data, documentAttributes: nil)!
let wholeRange = NSMakeRange(0, string.length)
var table: NSTextTable?
string.enumerateAttribute(.paragraphStyle, in: wholeRange) { value, range, _ in
guard let paragraphStyle = value as? NSParagraphStyle else { return }
let tableBlocks = paragraphStyle.textBlocks.compactMap { $0 as? NSTextTableBlock }
for block in tableBlocks {
if table == nil { table = block.table } // Keep first table found
let newBlock = block.copy(for: table!)
let newStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
newStyle.textBlocks = [newBlock]
string.addAttribute(.paragraphStyle, value: newStyle, range: range)
}
if tableBlocks.isEmpty { table = nil }
}
textView.textStorage!.setAttributedString(string)
}
}
extension NSTextTableBlock {
func copy(for table: NSTextTable) -> NSTextTableBlock {
let newBlock = NSTextTableBlock(table: table, startingRow: startingRow,
rowSpan: rowSpan, startingColumn: startingColumn, columnSpan: columnSpan)
newBlock.backgroundColor = backgroundColor
newBlock.verticalAlignment = verticalAlignment
for edge: NSRectEdge in [.minX, .minY, .maxX, .maxY] {
for layer: NSTextBlock.Layer in [.border, .margin, .padding] {
let width = width(for: layer, edge: edge)
let valueType = widthValueType(for: layer, edge: edge)
newBlock.setWidth(width, type: valueType, for: layer, edge: edge)
}
newBlock.setBorderColor(borderColor(for: edge), for: edge)
}
for dimension: NSTextBlock.Dimension in [.height, .maximumHeight, .maximumWidth, .minimumHeight, .minimumWidth, .width] {
let value = value(for: dimension)
let valueType = valueType(for: dimension)
newBlock.setValue(value, type: valueType, for: dimension)
}
return newBlock
}
}