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。
常见问题排查
- 更新bounds后没生效:记得调用
pdfView.setNeedsDisplay()刷新视图 - 高度计算不准:确保计算时用的字体和批注的字体完全一致
- 自定义绘制区域错位:绘制时用
self.bounds而不是固定值,避免和批注区域不匹配
内容的提问来源于stack exchange,提问作者user979331
相关产品推荐
相关产品推荐

