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

SwiftUI如何将View导出为多页A4尺寸PDF?

SwiftUI长视图导出多页A4 PDF问题修复

问题现象

尝试将无法在单页容纳的SwiftUI View导出为A4尺寸多页PDF时,仅第一页有内容,其余页面均为空白。

核心问题分析

  • 布局未完成:convertToScrollView中调用layoutIfNeeded()时,scrollView未添加到视图层级,导致HostingController的视图无法正确计算完整高度,contentSize不准确。
  • 截图逻辑错误:直接对scrollView截图并裁剪的方式无法捕获超出初始可见区域的内容,未渲染的部分不会出现在截图中。
  • PDF上下文操作顺序错误:代码中重复调用completion,且UIGraphicsEndPDFContext()的位置逻辑混乱,导致上下文提前结束。

修复方案

  1. 确保视图完整渲染:将scrollView临时添加到根视图,等待布局完成后再计算contentSize和生成PDF,之后移除临时视图。
  2. 直接绘制内容到PDF上下文:利用UIView的drawHierarchy(in:afterScreenUpdates:)直接在PDF上下文中绘制对应页面的内容,而非依赖截图。
  3. 修正PDF上下文操作顺序:确保UIGraphicsEndPDFContext()在所有页面绘制完成后调用,且只调用一次completion。

完整修正代码

import SwiftUI

extension View {
    func sharePDF<Content: View>(@ViewBuilder content: @escaping () -> Content, fileName: String) {
        exportPDF(content: content, completion: { status, url in
            if let url = url, status {
                ShareSheet.instance.share(items: [url])
            } else {
                print("⚠️ Failed to make PDF")
            }
        }, fileName: fileName)
    }
    
    fileprivate func convertToScrollView<Content: View>(@ViewBuilder content: @escaping () -> Content) -> UIScrollView {
        let scrollView = UIScrollView()
        scrollView.tag = 1009
        
        let hostingController = UIHostingController(rootView: content()).view!
        hostingController.translatesAutoresizingMaskIntoConstraints = false
        
        let constraints = [
            hostingController.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            hostingController.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            hostingController.topAnchor.constraint(equalTo: scrollView.topAnchor),
            hostingController.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            hostingController.widthAnchor.constraint(equalToConstant: 595.2) // A4宽度(72dpi)
        ]
        
        scrollView.addSubview(hostingController)
        scrollView.addConstraints(constraints)
        
        return scrollView
    }
    
    fileprivate func exportPDF<Content: View>(@ViewBuilder content: @escaping () -> Content, completion: @escaping (Bool, URL?) -> (), fileName: String) {
        let documentDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        let outputFileURL = documentDirectory.appendingPathComponent("\(fileName)\(UUID().uuidString).pdf")
        
        let scrollView = convertToScrollView {
            content()
        }
        scrollView.frame = CGRect(x: 0, y: 0, width: 595.2, height: 841.8) // A4尺寸
        
        // 临时添加到根视图以完成布局
        let rootController = getRootController()
        rootController.view.addSubview(scrollView)
        scrollView.layoutIfNeeded()
        
        let pageSize = scrollView.frame.size
        let contentSize = scrollView.contentSize
        let pageCount = Int(ceil(contentSize.height / pageSize.height))
        
        // 创建PDF上下文
        UIGraphicsBeginPDFContextToFile(outputFileURL.path, .zero, nil)
        
        for index in 0..<pageCount {
            UIGraphicsBeginPDFPageWithInfo(CGRect(origin: .zero, size: pageSize), nil)
            
            let currentContext = UIGraphicsGetCurrentContext()!
            // 平移上下文到对应页面的位置
            currentContext.translateBy(x: 0, y: -pageSize.height * CGFloat(index))
            
            // 直接绘制scrollView的内容到PDF上下文
            scrollView.drawHierarchy(in: scrollView.bounds, afterScreenUpdates: true)
        }
        
        // 结束PDF上下文
        UIGraphicsEndPDFContext()
        
        // 移除临时视图
        scrollView.removeFromSuperview()
        
        completion(true, outputFileURL)
    }
    
    fileprivate func getRootController() -> UIViewController {
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let root = screen.windows.first?.rootViewController else {
            return UIViewController()
        }
        return root
    }
}

extension UIView {
    // 保留原有截图方法(如果需要其他场景使用)
    func takeScreenshot() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
        drawHierarchy(in: bounds, afterScreenUpdates: true)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image ?? UIImage()
    }
}

// 假设ShareSheet是你已实现的分享组件
class ShareSheet: UIViewController {
    static let instance = ShareSheet()
    
    func share(items: [Any]) {
        let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
        activityViewController.modalPresentationStyle = .popover
        if let popover = activityViewController.popoverPresentationController {
            popover.sourceView = UIApplication.shared.windows.first?.rootViewController?.view
        }
        UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true)
    }
}

关键修复点说明

  • 临时添加视图到层级:确保scrollView及其子视图完成完整布局,获取准确的contentSize。
  • 上下文平移绘制:通过平移PDF上下文,直接绘制scrollView对应页面区域的内容,避免截图无法捕获未渲染区域的问题。
  • 修正操作顺序:确保PDF上下文在所有页面绘制完成后结束,且只调用一次completion,避免资源泄漏或逻辑错误。

内容的提问来源于stack exchange,提问作者Ioannis

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 04:47:23