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,而非仅导出按钮,恳请帮忙解决该问题。
解决方案
问题根源
- 视图层级错误:原
WorkListView的body中,按钮VStack和List是并列的顶级视图,没有被统一容器包裹,导致PDF渲染时无法识别完整的视图结构。 - 直接传递
self的问题:导出时传递self会包含视图的交互状态和未布局完成的元素,导致渲染不完整。 - 布局计算不充分: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
相关产品推荐
相关产品推荐

