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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 02:55:58