SwiftUI视图数组生成PDF遇多问题,求iOS身份卡PDF生成优化方案
问题分析与解决方案
核心问题拆解
- Sheet无输出:原代码采用异步队列生成PDF,但直接返回未完成渲染的空
PDFDocument,导致Sheet拿到的是无效文档。 - 速度缓慢:错误使用
PDFView添加UIHostingController子视图的方式生成PDF,频繁队列切换+低效视图层级操作导致性能瓶颈。 - 大量数据崩溃:未正确释放临时视图资源,内存峰值过高引发崩溃。
优化后的完整实现
基于Apple推荐的UIGraphicsPDFRenderer重构PDF生成逻辑,解决上述所有问题,同时满足你的设计目标:
1. 基础定义(PageSize枚举)
enum PageSize { case a4, letter, custom(CGSize) var size: CGSize { switch self { case .a4: return CGSize(width: 595.28, height: 841.89) // 72dpi标准尺寸 case .letter: return CGSize(width: 612, height: 792) // 72dpi标准尺寸 case .custom(let size): return size } } }
2. 重构后的PDFDataManager
import SwiftUI import PDFKit class PDFDataManager { static let shared = PDFDataManager() private init() {} struct Item<Content: View> { let views: [Content] let width: CGFloat let height: CGFloat } // 同步生成(适合小批量数据) func generate<Content: View>( from item: Item<Content>, paper: PageSize = .a4, margin: CGFloat = 36, bleed: CGFloat = 10 ) -> PDFDocument { let paperSize = paper.size let usableWidth = paperSize.width - margin * 2 - bleed * 2 let usableHeight = paperSize.height - margin * 2 - bleed * 2 let itemSize = CGSize(width: item.width, height: item.height) let maxCols = Int(floor(usableWidth / itemSize.width)) let maxRows = Int(floor(usableHeight / itemSize.height)) let itemsPerPage = maxCols * maxRows let pdfDocument = PDFDocument() let totalPages = Int(ceil(Double(item.views.count) / Double(itemsPerPage))) for pageIndex in 0..<totalPages { autoreleasepool { let renderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: paperSize)) let pdfData = renderer.pdfData { context in context.beginPage() // 转换PDF坐标系统(适配UIKit与PDF的Y轴方向差异) context.cgContext.translateBy(x: 0, y: paperSize.height) context.cgContext.scaleBy(x: 1, y: -1) // 计算当前页的视图范围 let startIndex = pageIndex * itemsPerPage let endIndex = min(startIndex + itemsPerPage, item.views.count) let pageItems = item.views[startIndex..<endIndex] var currentRow = 0 var currentCol = 0 for view in pageItems { // 计算视图位置(包含边距与出血) let x = margin + bleed + CGFloat(currentCol) * (itemSize.width + bleed * 2) let y = margin + bleed + CGFloat(currentRow) * (itemSize.height + bleed * 2) let rect = CGRect(x: x, y: y, width: itemSize.width, height: itemSize.height) // 高效渲染SwiftUI视图到PDF上下文 let hostingController = UIHostingController(rootView: view) hostingController.view.frame = rect hostingController.view.backgroundColor = .clear hostingController.view.drawHierarchy(in: rect, afterScreenUpdates: true) // 更新行列指针 currentCol += 1 if currentCol >= maxCols { currentCol = 0 currentRow += 1 } } } if let pdfPage = PDFPage(data: pdfData) { pdfDocument.insert(pdfPage, at: pageIndex) } } } return pdfDocument } // 异步生成(适合大批量数据,避免阻塞主线程) func generateAsync<Content: View>( from item: Item<Content>, paper: PageSize = .a4, margin: CGFloat = 36, bleed: CGFloat = 10, completion: @escaping (PDFDocument) -> Void ) { DispatchQueue.global(qos: .userInitiated).async { let document = self.generate(from: item, paper: paper, margin: margin, bleed: bleed) DispatchQueue.main.async { completion(document) } } } }
3. SwiftUI集成示例
struct ContentView: View { let cardSize = CGSize(width: 153.8, height: 242.1) @State private var selectedCards: [IdentityCard] = [] // 替换为你的身份卡视图数组 @State private var showingPDFSheet = false @State private var generatedPDF: PDFDocument? var body: some View { Button("生成PDF") { let pdfItem = PDFDataManager.Item( views: selectedCards, width: cardSize.width, height: cardSize.height ) // 小批量数据用同步生成 generatedPDF = PDFDataManager.shared.generate(from: pdfItem) showingPDFSheet = true // 大批量数据用异步生成 // PDFDataManager.shared.generateAsync(from: pdfItem) { pdf in // generatedPDF = pdf // showingPDFSheet = true // } } .sheet(isPresented: $showingPDFSheet) { if let pdf = generatedPDF { PDFViewWrapper(document: pdf) .edgesIgnoringSafeArea(.all) } } } } // SwiftUI包装PDFView用于预览 struct PDFViewWrapper: UIViewRepresentable { let document: PDFDocument func makeUIView(context: Context) -> PDFView { let pdfView = PDFView() pdfView.document = document pdfView.autoScales = true return pdfView } func updateUIView(_ uiView: PDFView, context: Context) {} } // 示例身份卡视图 struct IdentityCard: View { let name: String let position: String let avatar: Image var body: some View { ZStack { RoundedRectangle(cornerRadius: 8) .fill(.white) .border(.gray, width: 0.5) VStack(spacing: 12) { avatar.resizable().scaledToFit().frame(height: 80) Text(name).font(.headline) Text(position).font(.subheadline).foregroundColor(.gray) } .padding(16) } .frame(width: 153.8, height: 242.1) } }
4. 分享/打印/导出功能实现
// 分享PDF func sharePDF(_ document: PDFDocument) { guard let pdfData = document.dataRepresentation() else { return } let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("身份卡.pdf") do { try pdfData.write(to: tempURL) let activityVC = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil) // 在SwiftUI中获取根控制器展示分享面板 if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootVC = windowScene.windows.first?.rootViewController { rootVC.present(activityVC, animated: true) } } catch { print("PDF保存失败:\(error.localizedDescription)") } } // 打印PDF func printPDF(_ document: PDFDocument) { let printInfo = UIPrintInfo(dictionary: nil) printInfo.outputType = .general printInfo.jobName = "身份卡打印" let printController = UIPrintInteractionController.shared printController.printInfo = printInfo printController.printingItem = document.dataRepresentation() // 在SwiftUI中展示打印控制器 if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootVC = windowScene.windows.first?.rootViewController { printController.present(from: .zero, in: rootVC.view, animated: true) } }
关键优化点说明
- 高效渲染:用
drawHierarchy直接将SwiftUI视图渲染到PDF上下文,避免PDFView子视图层级的额外开销。 - 内存控制:每页渲染用
autoreleasepool释放临时资源,大幅降低内存峰值。 - 坐标适配:处理PDF与UIKit的Y轴方向差异,确保视图位置正确。
- 同步/异步可选:小批量数据用同步生成,大批量数据用异步避免阻塞主线程。
内容的提问来源于stack exchange,提问作者markb
相关产品推荐
相关产品推荐

