如何在PDFKit中设置PDF锚点,实现iOS文件应用式PDF视图效果?
实现iOS Files风格的PDF查看器效果
需求说明
需要实现和iOS Files应用完全一致的PDF查看器效果,核心功能点:
- 点击视图时切换导航栏显示/隐藏,隐藏导航栏时PDF顶部位置保持不变
- 放大PDF时可填满整个屏幕,缩放回原比例后顶部回到初始位置
- 适配横屏边缘(当前先聚焦顶部需求)
用户提供的初始代码
ContentView实现
@MainActor struct ContentView: View { @State var showBars: Bool = true @State var pdfUrl: URL? var body: some View { NavigationStack { GeometryReader { geo in ScrollView { TabView { if let url = pdfUrl { PDFViewer(pdfUrl: url) .onTapGesture { withAnimation { showBars.toggle() } } } } .tabViewStyle(.page(indexDisplayMode: .never)) .frame(width: geo.size.width, height: geo.size.height) } .scrollDisabled(true) } .ignoresSafeArea(edges: !showBars ? .all : []) } .task { pdfUrl = renderPDF() } } func renderPDF() -> URL { let renderer = ImageRenderer(content: VStack {}) let url = URL.documentsDirectory.appending(path: "samplepdf.pdf") renderer.render { size, context in guard let pdf = CGContext(url as CFURL, mediaBox: nil, nil) else { return } pdf.beginPDFPage(nil) context(pdf) pdf.endPDFPage() pdf.beginPDFPage(nil) context(pdf) pdf.endPDFPage() pdf.closePDF() } return url } }
PDFViewer相关实现
struct PDFViewer: View { var pdfUrl: URL var body: some View { PDFSheetView(document: .init(url: pdfUrl)) } } class PDFViewController: UIViewController { let document: PDFDocument? var pdfView: PDFView! init(document: PDFDocument?) { self.document = document super.init(nibName: nil, bundle: nil) } override func loadView() { let view = PDFView() self.view = view self.pdfView = view view.document = document view.displayDirection = .vertical view.autoScales = true view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true } required init?(coder: NSCoder) { document = nil super.init(coder: coder) return nil } override func viewDidLayoutSubviews() { let bounds = view.bounds if let document { if let page = document.page(at: 0) { let pageBounds = page.bounds(for: .mediaBox) if bounds.width > 0 && pageBounds.width > 0 { let scaleFactor = bounds.width / pageBounds.width let subtractionFactor = scaleFactor * 0.0125 pdfView.minScaleFactor = scaleFactor - subtractionFactor } } } } } struct PDFSheetView: UIViewControllerRepresentable { typealias UIViewControllerType = PDFViewController let document: PDFDocument? func makeUIViewController(context: Context) -> PDFViewController { let controller = PDFViewController(document: document) return controller } func updateUIViewController(_ uiViewController: PDFViewController, context: Context) { } }
解决方案
可以完全实现该效果,以下是针对性修改方案:
1. 重构ContentView布局
移除冗余的GeometryReader和ScrollView,直接通过导航栏状态控制SafeArea,避免布局冲突:
@MainActor struct ContentView: View { @State var showBars: Bool = true @State var pdfUrl: URL? var body: some View { NavigationStack { ZStack { if let url = pdfUrl { PDFViewer(pdfUrl: url) .ignoresSafeArea(!showBars) .onTapGesture { withAnimation { showBars.toggle() } } } } .navigationBarHidden(!showBars) } .task { pdfUrl = renderPDF() } } func renderPDF() -> URL { let renderer = ImageRenderer(content: VStack { Text("Sample PDF Content") .font(.largeTitle) .padding() }) let url = URL.documentsDirectory.appending(path: "samplepdf.pdf") renderer.render { size, context in guard let pdf = CGContext(url as CFURL, mediaBox: nil, nil) else { return } pdf.beginPDFPage(nil) context(pdf) pdf.endPDFPage() pdf.beginPDFPage(nil) context(pdf) pdf.endPDFPage() pdf.closePDF() } return url } }
2. 修改PDFViewController的布局与缩放逻辑
核心调整PDFView的约束绑定,记录初始位置并同步SafeArea状态变化:
struct PDFViewer: View { var pdfUrl: URL var body: some View { PDFSheetView(document: .init(url: pdfUrl)) } } class PDFViewController: UIViewController { let document: PDFDocument? var pdfView: PDFView! private var initialTopOffset: CGFloat = 0 init(document: PDFDocument?) { self.document = document super.init(nibName: nil, bundle: nil) } override func loadView() { let view = UIView() self.view = view pdfView = PDFView() pdfView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(pdfView) // 让PDFView直接填充父视图,脱离SafeArea绑定 NSLayoutConstraint.activate([ pdfView.topAnchor.constraint(equalTo: view.topAnchor), pdfView.leadingAnchor.constraint(equalTo: view.leadingAnchor), pdfView.trailingAnchor.constraint(equalTo: view.trailingAnchor), pdfView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) pdfView.document = document pdfView.displayDirection = .vertical pdfView.autoScales = true pdfView.displayMode = .singlePageContinuous pdfView.usePageViewController(true) pdfView.pageBreakMargins = .zero } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() guard let document = document, let firstPage = document.page(at: 0) else { return } let pageBounds = firstPage.bounds(for: .mediaBox) let viewBounds = view.bounds guard viewBounds.width > 0, pageBounds.width > 0 else { return } let scaleFactor = viewBounds.width / pageBounds.width pdfView.minScaleFactor = scaleFactor * 0.98 // 仅首次布局记录初始顶部偏移 if initialTopOffset == 0 { initialTopOffset = pdfView.documentView?.frame.origin.y ?? 0 } } // 同步SafeArea状态变化,调整PDF位置 func adjustForSafeAreaChange(_ ignoresSafeArea: Bool) { guard let documentView = pdfView.documentView else { return } if ignoresSafeArea { documentView.frame.origin.y = documentView.frame.origin.y } else { documentView.frame.origin.y = initialTopOffset } pdfView.layoutIfNeeded() } required init?(coder: NSCoder) { document = nil super.init(coder: coder) } } struct PDFSheetView: UIViewControllerRepresentable { typealias UIViewControllerType = PDFViewController let document: PDFDocument? var ignoresSafeArea: Bool = false func makeUIViewController(context: Context) -> PDFViewController { PDFViewController(document: document) } func updateUIViewController(_ uiViewController: PDFViewController, context: Context) { uiViewController.adjustForSafeAreaChange(ignoresSafeArea) } func ignoresSafeArea(_ ignore: Bool) -> PDFSheetView { var copy = self copy.ignoresSafeArea = ignore return copy } }
核心实现逻辑
- 让PDFView直接填充父视图,避免与SafeArea绑定导致的布局跳动
- 记录PDF初始顶部偏移,确保缩放回原比例时能回归初始位置
- 通过
navigationBarHidden与ignoresSafeArea联动,实现导航栏切换时PDF位置稳定 - 优化缩放比例计算,让初始缩放匹配屏幕宽度,同时限制最小缩放比例防止内容过小
内容的提问来源于stack exchange,提问作者JasonK
相关产品推荐
相关产品推荐

