SwiftUI中如何将嵌套字符串数组渲染为PDF?
问题描述
尝试在SwiftUI中将包含字符串数组的嵌套数组渲染为PDF文档,但仅能渲染Text组件,无法正常渲染列表。使用的代码如下:
@MainActor func render() -> URL { let renderer = ImageRenderer(content: List { ForEach(docnote.indices, id: \.self) { index in VStack(alignment: .leading) { ForEach(docnote[index], id: \.self) { info in Text(info) } } } .onDelete(perform: deleteStorageItem) // Add onDelete modifier for second List } ) let url = URL.documentsDirectory.appending(path: "output.pdf") renderer.render { size, context in var box = CGRect(x: 0, y: 0, width: size.width, height: size.height) guard let pdf = CGContext(url as CFURL, mediaBox: &box, nil) else { return } pdf.beginPDFPage(nil) context(pdf) pdf.endPDFPage() pdf.closePDF() } return url }
遇到的错误:
Unable to render flattened version of ListRepresentable<CollectionViewListDataSource, SelectionManagerBox>.
Unable to render flattened version of ListRepresentable<CollectionViewListDataSource, SelectionManagerBox>.
2023-04-15 20:13:41.812705-0400 PrescriptionTracker[4906:1289529] [ShareSheet] Only support loading options for CKShare and SWY types.
2023-04-15 20:13:41.843331-0400 PrescriptionTracker[4906:1289529] [ShareSheet] error fetching item for URL:file:///var/mobile/Containers/Data/Application/739F6D91-7F58-48DA-B3FB-1F9A4F0BE8BA/Documents/output.pdf : Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened."
问题原因
核心问题是ImageRenderer无法正确渲染SwiftUI的List组件——List底层依赖UIKit的UICollectionView实现,属于带交互的复杂组件,ImageRenderer无法将其扁平化处理生成PDF。而文件打不开的问题,本质是List渲染失败导致PDF文件未被正确写入。
解决方案
用纯SwiftUI的ScrollView + VStack组合替代List,这类轻量视图能被ImageRenderer正常解析。同时优化PDF渲染逻辑,确保文件正确生成:
@MainActor func render() -> URL { // 替换List为ScrollView+VStack,避免UIKit组件渲染限制 let contentView = ScrollView { VStack(spacing: 16) { ForEach(docnote.indices, id: \.self) { index in VStack(alignment: .leading, spacing: 8) { ForEach(docnote[index], id: \.self) { info in Text(info) .frame(maxWidth: .infinity, alignment: .leading) } } .padding(.horizontal) // 手动添加分隔线模拟List效果(可选) Divider() } } .padding() } // 设置固定PDF页面尺寸(A4标准) let pageSize = CGSize(width: 595, height: 842) let renderer = ImageRenderer(content: contentView) renderer.proposedSize = ProposedViewSize(pageSize) let url = URL.documentsDirectory.appending(path: "output.pdf") // 先删除旧文件,避免写入冲突 try? FileManager.default.removeItem(at: url) renderer.render { size, context in var box = CGRect(origin: .zero, size: size) guard let pdf = CGContext(url as CFURL, mediaBox: &box, nil) else { return } pdf.beginPDFPage(nil) context(pdf) pdf.endPDFPage() pdf.closePDF() } return url }
额外说明
- 如果需要模拟List的分隔线效果,可手动添加
Divider()组件 - 上述代码仅处理单页PDF,若内容过长需要分页,需额外计算内容高度并拆分渲染
- 移除了
onDelete修饰符,PDF属于静态文档,不需要交互功能,编辑逻辑应在生成PDF前处理
内容的提问来源于stack exchange,提问作者Shagufta

