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

Swift 4中如何让PDFAnnotation文本控件适配文本尺寸?

解决PDF多行文本批注自动适配高度的方案

我刚好解决过类似的需求,完全贴合iOS原生PDF批注"固定宽度、换行增高度"的交互逻辑,给你一套完整的实现方案:

核心思路

原生PDF批注的逻辑是锁定宽度,根据文本内容实时计算所需高度,然后更新批注的显示区域,不需要移除重新添加批注(那样会丢失编辑状态),直接修改bounds属性就能生效。


步骤1:完善自定义TextAnnotation类

首先优化你的自定义批注类,加入固定宽度和文本高度计算的核心方法:

class TextAnnotation: PDFAnnotation {
    // 固定批注宽度,和原生逻辑一致,不拉伸宽度
    let fixedWidth: CGFloat = 600
    // 存储当前文本,避免重复计算
    var currentText: String?
    
    override init(bounds: CGRect, forType annotationType: PDFAnnotationSubtype, withProperties properties: [AnyHashable : Any]?) {
        // 初始化时强制使用固定宽度,高度先设初始值
        let initialBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: fixedWidth, height: 100)
        super.init(bounds: initialBounds, forType: annotationType, withProperties: properties)
        
        self.widgetFieldType = .text
        self.font = UIFont.systemFont(ofSize: 80)
        self.fontColor = .red
        self.isMultiline = true
        self.widgetStringValue = "Text Here"
        self.currentText = self.widgetStringValue
        
        // 初始化就计算一次正确高度
        updateBoundsToFitText()
    }
    
    // 核心方法:计算文本需要的高度并更新bounds
    func updateBoundsToFitText() {
        guard let text = widgetStringValue, let font = font else { return }
        
        // 计算文本在固定宽度下的渲染高度(用最大高度来让文本自动换行)
        let textRect = text.boundingRect(
            with: CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude),
            options: .usesLineFragmentOrigin,
            attributes: [.font: font],
            context: nil
        )
        
        // 预留20pt上下边距,避免文字贴边
        let requiredHeight = textRect.height + 20
        // 只修改高度,保持位置和宽度不变
        var newBounds = self.bounds
        newBounds.size.height = requiredHeight
        self.bounds = newBounds
    }
    
    override func draw(with box: PDFDisplayBox, in context: CGContext) {
        // 先调用父类方法,保证原生的输入交互正常
        super.draw(with: box, in: context)
        
        // 自定义绘制(如果不需要可以删掉这部分)
        UIGraphicsPushContext(context)
        context.saveGState()
        
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .center
        let attributes: [NSAttributedString.Key: Any] = [
            .paragraphStyle: paragraphStyle,
            .font: font ?? UIFont.systemFont(ofSize: 80),
            .foregroundColor: fontColor ?? .red
        ]
        
        // 用更新后的bounds作为绘制区域,加10pt内边距
        widgetStringValue?.draw(in: self.bounds.insetBy(dx: 10, dy: 10), withAttributes: attributes)
        
        context.restoreGState()
        UIGraphicsPopContext()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

步骤2:在ViewController中监听文本变化,实时更新高度

PDF批注的文本变化不会自动触发bounds更新,所以需要用两种方式之一来监听:

方式1:编辑结束后更新(性能友好)

用PDFViewDelegate的方法监听编辑结束事件,适合对性能要求高的场景:

class YourPDFViewController: UIViewController, PDFViewDelegate {
    var pdfView: PDFView!
    var textAnnotation: TextAnnotation?

    override func viewDidLoad() {
        super.viewDidLoad()
        // 初始化你的pdfView...
        pdfView.delegate = self
        
        // 添加批注
        addTextAnnotation()
    }
    
    func addTextAnnotation() {
        guard let page = pdfView.currentPage else { return }
        textAnnotation = TextAnnotation(
            bounds: CGRect(x: 300, y: 200, width: 600, height: 100),
            forType: .widget,
            withProperties: nil
        )
        page.addAnnotation(textAnnotation!)
    }
    
    // 监听批注编辑结束
    func pdfView(_ pdfView: PDFView, didEndEditingAnnotation annotation: PDFAnnotation) {
        guard let textAnnot = annotation as? TextAnnotation else { return }
        // 只有文本变化时才更新,避免无效操作
        if textAnnot.currentText != textAnnot.widgetStringValue {
            textAnnot.currentText = textAnnot.widgetStringValue
            textAnnot.updateBoundsToFitText()
            // 刷新PDFView让新的bounds生效
            pdfView.setNeedsDisplay()
        }
    }
}

方式2:实时更新(和原生体验一致)

用KVO监听widgetStringValue的变化,用户输入时就实时调整高度:

// 在YourPDFViewController中添加以下代码
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    guard let textAnnot = textAnnotation else { return }
    // 监听文本变化
    textAnnot.addObserver(self, forKeyPath: "widgetStringValue", options: .new, context: nil)
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    textAnnotation?.removeObserver(self, forKeyPath: "widgetStringValue")
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "widgetStringValue", let textAnnot = object as? TextAnnotation {
        textAnnot.updateBoundsToFitText()
        pdfView.setNeedsDisplay()
    }
}

关于UITextField的类似实现

如果是UITextView(更推荐,因为UITextField的多行支持有限),思路完全一致:固定宽度,监听文本变化,计算高度更新frame:

class AutoHeightTextView: UITextView {
    let fixedWidth: CGFloat = 600
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        let initialFrame = CGRect(x: frame.origin.x, y: frame.origin.y, width: fixedWidth, height: 100)
        super.init(frame: initialFrame, textContainer: textContainer)
        self.font = UIFont.systemFont(ofSize: 80)
        self.textColor = .red
        self.isScrollEnabled = false // 必须禁用滚动,否则高度不会自动调整
        self.text = "Text Here"
        
        // 监听文本变化通知
        NotificationCenter.default.addObserver(self, selector: #selector(updateHeight), name: UITextView.textDidChangeNotification, object: self)
        // 初始化时更新高度
        updateHeight()
    }
    
    @objc func updateHeight() {
        guard let text = self.text, let font = self.font else { return }
        let textRect = text.boundingRect(
            with: CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude),
            options: .usesLineFragmentOrigin,
            attributes: [.font: font],
            context: nil
        )
        // 加边距避免贴边
        self.frame.size.height = textRect.height + 20
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

如果一定要用UITextField,需要设置isMultiline = true,然后监听EditingChanged事件,计算逻辑和上面一致,但UITextField的多行交互体验不如UITextView。


常见问题排查

  1. 更新bounds后没生效:记得调用pdfView.setNeedsDisplay()刷新视图
  2. 高度计算不准:确保计算时用的字体和批注的字体完全一致
  3. 自定义绘制区域错位:绘制时用self.bounds而不是固定值,避免和批注区域不匹配

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 09:22:58