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

Swift iOS:PDF渲染至UIView后外部超链接无响应问题求助

自定义PDF渲染视图中保留链接注解并实现跳转的解决方案

问题背景

iOS开发中,需将带Youtube外部链接的PDF首页渲染到自定义CanvasView(支持擦除、剪切等操作,无法使用系统PDFView),但出现两个问题:

  • 把原PDFPage转成Data后,新生成的PDFPage丢失所有链接注解
  • 即使直接传入原PDFPage,点击链接区域无任何响应

核心原因

  1. 注解丢失原因:原代码用UIGraphicsPDFRenderer将PDFPage绘制到上下文生成新PDF,这个过程仅保留像素绘制结果,不会保留原PDF的结构化注解数据(包括链接)。
  2. 点击无响应原因:自定义CanvasView仅实现了PDF内容的绘制,没有监听点击事件,也没有将屏幕点击坐标映射到PDF页面坐标,更没有判断是否命中链接注解的逻辑。

解决方案

1. 保留PDF注解的单页Data提取方式

不要通过绘制生成新PDF,而是直接从原PDFDocument中提取目标页面到新的PDFDocument,再转成Data,这样能完整保留注解数据。

2. 实现链接点击的交互逻辑

在CanvasView中添加点击手势,将屏幕点击坐标转换为PDF页面的坐标空间,遍历页面的链接注解,判断点击区域是否命中注解,若命中则打开对应的URL。

修改后的代码

ViewController.swift

import UIKit
import PDFKit

class ViewController: UIViewController {

    var pdfDocument: PDFDocument?

    override func loadView() {
        super.loadView()
    }
    
    // 替换原getData方法:提取单页到新PDFDocument,保留注解
    func getData(from page: PDFPage) -> Data? {
        let newDocument = PDFDocument()
        newDocument.insert(page, at: 0)
        return newDocument.dataRepresentation()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        let fileUrl = Bundle.main.url(forResource: "Doc", withExtension: "pdf")
        guard let fileUrl else { return }
        pdfDocument = PDFDocument(url: fileUrl)
        guard let firstPage = pdfDocument?.page(at: 0) else { return }
        
        // 用新方法获取带注解的Data
        guard let pdfData = getData(from: firstPage) else { return }
        let canvasView = CanvasView(data: pdfData)
        self.view.addSubview(canvasView)
        
        NSLayoutConstraint.activate([
            canvasView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
            canvasView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
            canvasView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
            canvasView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor)
        ])
    }
}

CanvasView.swift

import UIKit
import PDFKit

class CanvasView: UIView {
    
    var page: PDFPage?
    private let tapGesture = UITapGestureRecognizer()
    
    init(data: Data) {
        super.init(frame: .zero)
        translatesAutoresizingMaskIntoConstraints = false
        page = loadPDFPage(pdfData: data)
        setupTapGesture()
    }
    
    init(pdfPage: PDFPage){
        super.init(frame: .zero)
        translatesAutoresizingMaskIntoConstraints = false
        page = pdfPage
        setupTapGesture()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func loadPDFPage(pdfData: Data) -> PDFPage? {
        guard let document = PDFDocument(data: pdfData) else { return nil }
        return document.page(at: 0)
    }
    
    // 设置点击手势
    private func setupTapGesture() {
        tapGesture.addTarget(self, action: #selector(handleTap(_:)))
        addGestureRecognizer(tapGesture)
    }
    
    // 处理点击事件
    @objc private func handleTap(_ gesture: UITapGestureRecognizer) {
        guard let page = page else { return }
        let tapPoint = gesture.location(in: self)
        
        // 将屏幕坐标转换为PDF页面坐标
        let cropBox = page.bounds(for: .cropBox)
        let scaleX = cropBox.width / bounds.width
        let scaleY = cropBox.height / bounds.height
        
        // 考虑坐标系翻转:UIKit原点在左上,PDF原点在左下
        let pdfPoint = CGPoint(
            x: tapPoint.x * scaleX,
            y: (bounds.height - tapPoint.y) * scaleY
        )
        
        // 遍历页面的链接注解
        for annotation in page.annotations where annotation.type == .link {
            guard let linkAnnotation = annotation as? PDFAnnotationLink,
                  let url = linkAnnotation.url else { continue }
            
            // 获取注解在PDF页面的矩形
            let annotationRect = linkAnnotation.bounds
            
            // 判断点击点是否在注解区域内
            if annotationRect.contains(pdfPoint) {
                // 打开URL
                UIApplication.shared.open(url)
                break
            }
        }
    }

    override func draw(_ layer: CALayer, in ctx: CGContext) {
        guard let page = page, let cgPDFPage = page.pageRef else { return }
        
        let cropBoxBounds = page.bounds(for: .cropBox)
        let scaleX = layer.bounds.width / cropBoxBounds.width
        let scaleY = layer.bounds.height / cropBoxBounds.height
        
        ctx.saveGState()
        ctx.scaleBy(x: scaleX, y: scaleY)
        ctx.translateBy(x: -cropBoxBounds.origin.x, y: cropBoxBounds.height + cropBoxBounds.origin.y )
        ctx.scaleBy(x: 1, y: -1)
        ctx.setFillColor(UIColor.white.cgColor)
        ctx.fill(cropBoxBounds)
        ctx.drawPDFPage(cgPDFPage)
        ctx.restoreGState()
    }

    override func draw(_ rect: CGRect) {}
}

验证说明

  1. 提取的单页Data生成的PDFPage会保留原有的链接注解,可通过打印page.annotations.count验证
  2. 点击PDF中的链接区域时,会自动跳转至对应的Youtube页面
  3. 原有的自定义绘制(擦除、剪切等)逻辑可正常复用

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 05:45:00