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

SwiftUI中WorkListView导出PDF异常,仅导出按钮问题求助

问题:WorkListView导出PDF仅显示按钮,无法完整导出内容

我在将WorkListView导出为PDF时遇到问题,目前仅能导出按钮部分。以下是我的代码示例:

Client 数据模型

class Client: ObservableObject, Identifiable {
    let name = "Foo"
    @Published var work: [Work]
    
    init() {
        self.work = []
    }

    func addWork(_ work: Work) {
        self.work.append(work)
    }
}

Work 数据模型

struct Work: Identifiable {
    var id = UUID()
    let content = "Bar"
}

WorkListView 视图

struct WorkListView: View {
    @State var client: Client
    @ObservedObject var clientData = ClientData()
    @State private var showSheet = false
    
    var body: some View {
VStack {
                Button {
                    exportPDF {
                        self
                    } completion: { status, url in
                        if let url = url, status {
                            self.PDFUrl = url
                            self.showShareSheet.toggle()
                        }
                        else {
                            print("Failed to produce PDF")
                        }
                    }
                } label: {
                    Image(systemName: "square.and.arrow.up.fill")
                        .font(.title3)
                }
            }
        List {
    Section {
        Text(client.name)
    }
    Section(header: Text("Performed Work")) {
        ForEach(client.work.indices) { work in
            VStack  {
                Text(client.work[work].content)
            }
        }
    }
        }
        .onTapGesture {
            showSheet.toggle()
        }
        .sheet(isPresented: $showSheet, content: {
            AddWorkView(client: client, showSheet: $showSheet)
        })
        .padding()
    }
}

PDF 导出扩展

extension View {
    func convertToScrollView<Content: View>(@ViewBuilder content: @escaping ()->Content)->UIScrollView {
            
        let scrollView = UIScrollView()
            
        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: UIScreen.main.bounds.width)
        ]
        scrollView.addSubview(hostingController)
        scrollView.addConstraints(constraints)
        scrollView.layoutIfNeeded()
            
        return scrollView
    }


    func exportPDF<Content: View>(@ViewBuilder content: @escaping ()->Content, completion: @escaping (Bool,URL?)->()){

        let documentDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
        let outputFileURL = documentDirectory.appendingPathComponent("YOURPDFNAME\(UUID().uuidString).pdf")

        let pdfView = convertToScrollView {
            content()
        }
        pdfView.tag = 1009
        let size = pdfView.contentSize
        pdfView.frame = CGRect(x: 0, y: getSafeArea().top, width: size.width, height: size.height)

        getRootController().view.insertSubview(pdfView, at: 0)

        let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: size.width, height: size.height))

        do {

            try renderer.writePDF(to: outputFileURL, withActions: { context in

                context.beginPage()
                pdfView.layer.render(in: context.cgContext)
            })
            completion(true,outputFileURL)
        }
        catch {
            completion(false,nil)
            print(error.localizedDescription)
        }

        getRootController().view.subviews.forEach { view in
            if view.tag == 1009 {
                print("Removed")
                view.removeFromSuperview()
            }
        }
    }

    
    func screenBounds()->CGRect {
        return UIScreen.main.bounds
    }
    
    func getRootController()->UIViewController {
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return .init()
        }
        guard let root = screen.windows.first?.rootViewController else {
            return .init()
        }
        return root
    }
    
    func getSafeArea()->UIEdgeInsets {
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return .zero
        }
        guard let safeArea = screen.windows.first?.safeAreaInsets else {
            return .zero
        }
        return safeArea
    }
}

我还有一个用于切换分享面板的结构体。我希望能完整导出WorkListView为PDF,而非仅导出按钮,恳请帮忙解决该问题。


解决方案

问题根源

  1. 视图层级错误:原WorkListView的body中,按钮VStack和List是并列的顶级视图,没有被统一容器包裹,导致PDF渲染时无法识别完整的视图结构。
  2. 直接传递self的问题:导出时传递self会包含视图的交互状态和未布局完成的元素,导致渲染不完整。
  3. 布局计算不充分:PDF渲染前未强制视图完成布局,部分内容因异步布局未被捕获。

修复方案

1. 重构视图结构,分离导出内容

创建单独的内容视图用于PDF导出,避免包含按钮等无关元素,同时确保主视图层级统一。

2. 优化PDF导出逻辑

强制视图完成布局计算,确保内容尺寸正确获取,避免因布局未完成导致的渲染缺失。

修改后的完整代码

WorkListView 修正版

struct WorkListView: View {
    @State var client: Client
    @ObservedObject var clientData = ClientData()
    @State private var showSheet = false
    @State private var pdfUrl: URL?
    @State private var showShareSheet = false
    
    var body: some View {
        VStack(spacing: 16) {
            // 导出按钮
            Button {
                exportPDF {
                    // 传递仅包含核心内容的视图
                    WorkListExportContent(client: client)
                } completion: { status, url in
                    if let url = url, status {
                        self.pdfUrl = url
                        self.showShareSheet.toggle()
                    } else {
                        print("Failed to produce PDF")
                    }
                }
            } label: {
                Image(systemName: "square.and.arrow.up.fill")
                    .font(.title3)
            }
            
            // 主内容列表
            List {
                Section {
                    Text(client.name)
                }
                Section(header: Text("Performed Work")) {
                    ForEach(client.work) { work in
                        Text(work.content)
                    }
                }
            }
        }
        .padding()
        .onTapGesture {
            showSheet.toggle()
        }
        .sheet(isPresented: $showSheet) {
            AddWorkView(client: client, showSheet: $showSheet)
        }
        .sheet(isPresented: $showShareSheet) {
            if let url = pdfUrl {
                ShareSheet(activityItems: [url])
            }
        }
    }
}

// 专门用于导出的内容视图
struct WorkListExportContent: View {
    let client: Client
    
    var body: some View {
        VStack {
            List {
                Section {
                    Text(client.name)
                }
                Section(header: Text("Performed Work")) {
                    ForEach(client.work) { work in
                        Text(work.content)
                    }
                }
            }
            .listStyle(.insetGrouped)
        }
        .frame(width: UIScreen.main.bounds.width)
        .padding()
    }
}

// 分享面板实现示例
struct ShareSheet: UIViewControllerRepresentable {
    let activityItems: [Any]
    
    func makeUIViewController(context: Context) -> UIActivityViewController {
        UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
    }
    
    func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}

PDF导出扩展优化版

extension View {
    func convertToScrollView<Content: View>(@ViewBuilder content: @escaping ()->Content)->UIScrollView {
        let scrollView = UIScrollView()
        let hostingController = UIHostingController(rootView: content())
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        
        scrollView.addSubview(hostingController.view)
        
        NSLayoutConstraint.activate([
            hostingController.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            hostingController.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
        ])
        
        // 强制完成布局,确保内容尺寸正确
        scrollView.layoutIfNeeded()
        hostingController.view.layoutIfNeeded()
        
        return scrollView
    }

    func exportPDF<Content: View>(@ViewBuilder content: @escaping ()->Content, completion: @escaping (Bool,URL?)->()){
        guard let documentDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
            completion(false, nil)
            return
        }
        let outputFileURL = documentDirectory.appendingPathComponent("WorkList_\(UUID().uuidString).pdf")

        let pdfView = convertToScrollView {
            content()
        }
        let contentSize = pdfView.contentSize
        
        // 设置正确的视图frame
        pdfView.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
        
        let rootController = getRootController()
        rootController.view.insertSubview(pdfView, at: 0)
        rootController.view.layoutIfNeeded()

        let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height))

        do {
            try renderer.writePDF(to: outputFileURL, withActions: { context in
                context.beginPage()
                pdfView.layer.render(in: context.cgContext)
            })
            completion(true, outputFileURL)
        } catch {
            print("PDF导出失败: \(error.localizedDescription)")
            completion(false, nil)
        }

        pdfView.removeFromSuperview()
    }

    func getRootController()->UIViewController {
        guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let window = scene.windows.first,
              let rootVC = window.rootViewController else {
            return UIViewController()
        }
        return rootVC
    }
}

关键改进点

  • 分离导出内容与交互视图,确保PDF仅包含核心业务内容
  • 统一主视图层级,修复布局混乱问题
  • 强制布局完成,避免异步布局导致的内容缺失
  • 优化PDF渲染的尺寸计算逻辑,确保内容完整显示

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 06:12:57