You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI视图数组生成PDF遇多问题,求iOS身份卡PDF生成优化方案

问题分析与解决方案

核心问题拆解

  1. Sheet无输出:原代码采用异步队列生成PDF,但直接返回未完成渲染的空PDFDocument,导致Sheet拿到的是无效文档。
  2. 速度缓慢:错误使用PDFView添加UIHostingController子视图的方式生成PDF,频繁队列切换+低效视图层级操作导致性能瓶颈。
  3. 大量数据崩溃:未正确释放临时视图资源,内存峰值过高引发崩溃。

优化后的完整实现

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 21:47:00